@arraypress/waveform-player 1.5.2 → 1.6.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.
- package/README.md +48 -0
- package/dist/waveform-player.esm.js +2 -2
- package/dist/waveform-player.js +191 -41
- package/dist/waveform-player.min.js +2 -2
- package/package.json +1 -1
- package/src/js/core.js +238 -51
- package/src/js/themes.js +7 -0
package/README.md
CHANGED
|
@@ -228,6 +228,50 @@ Choose from 6 built-in styles:
|
|
|
228
228
|
| `markers` | array | `[]` | Chapter markers array |
|
|
229
229
|
| `waveform` | array | `null` | Pre-generated waveform data |
|
|
230
230
|
| `enableMediaSession` | boolean | `true` | Enable system media controls |
|
|
231
|
+
| `audioMode` | string | `'self'` | `'self'` (own `<audio>`) or `'external'` (delegate) |
|
|
232
|
+
|
|
233
|
+
### External audio mode
|
|
234
|
+
|
|
235
|
+
When `audioMode: 'external'` the player skips creating its own `<audio>` element and becomes a visualization-only surface. Play / pause / seek interactions dispatch cancelable events on the container, and an external controller (e.g. [`@arraypress/waveform-bar`](https://github.com/arraypress/waveform-bar) 1.3+) handles audio + pumps state back via `setPlayingState()` / `setProgress()`.
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<!-- The bar discovers this on init, listens for request-play events,
|
|
239
|
+
and mirrors its own playback progress back into the canvas. -->
|
|
240
|
+
<div data-waveform-player
|
|
241
|
+
data-audio-mode="external"
|
|
242
|
+
data-url="song.mp3"
|
|
243
|
+
data-waveform-style="bars"
|
|
244
|
+
data-wb-play
|
|
245
|
+
data-wb-url="song.mp3"
|
|
246
|
+
data-wb-title="..."></div>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// Or construct directly:
|
|
251
|
+
const player = new WaveformPlayer(el, { audioMode: 'external' });
|
|
252
|
+
|
|
253
|
+
// Listen for the play action and route to your own audio source:
|
|
254
|
+
el.addEventListener('waveformplayer:request-play', (e) => {
|
|
255
|
+
// e.detail = { url, title, subtitle, artist, artwork, id, player }
|
|
256
|
+
yourAudio.src = e.detail.url;
|
|
257
|
+
yourAudio.play();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Drive the visualization from your audio's timeupdate:
|
|
261
|
+
yourAudio.addEventListener('timeupdate', () => {
|
|
262
|
+
player.setProgress(yourAudio.currentTime, yourAudio.duration);
|
|
263
|
+
});
|
|
264
|
+
yourAudio.addEventListener('play', () => player.setPlayingState(true));
|
|
265
|
+
yourAudio.addEventListener('pause', () => player.setPlayingState(false));
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Events dispatched in external mode (all bubble; all cancelable):
|
|
269
|
+
|
|
270
|
+
| Event | Detail |
|
|
271
|
+
|------------------------------------|-----------------------------------------------------|
|
|
272
|
+
| `waveformplayer:request-play` | `{ url, title, subtitle, artist, artwork, id, player }` |
|
|
273
|
+
| `waveformplayer:request-pause` | Same shape as request-play |
|
|
274
|
+
| `waveformplayer:request-seek` | Same shape + `{ percent: 0..1 }` |
|
|
231
275
|
|
|
232
276
|
## API Methods
|
|
233
277
|
|
|
@@ -247,6 +291,10 @@ player.setVolume(0.8); // 80% volume
|
|
|
247
291
|
// Speed
|
|
248
292
|
player.setPlaybackRate(1.5); // 1.5x speed
|
|
249
293
|
|
|
294
|
+
// External-mode state pumps (only useful when audioMode === 'external')
|
|
295
|
+
player.setPlayingState(true); // toggle play/pause UI state
|
|
296
|
+
player.setProgress(currentTime, duration); // sync canvas + time displays
|
|
297
|
+
|
|
250
298
|
// Dynamic loading
|
|
251
299
|
await player.loadTrack('new-song.mp3', 'New Title', 'New Artist', {
|
|
252
300
|
markers: [{time: 30, label: 'Chorus'}],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function A(t){let e={};if(t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.preload&&(e.preload=t.dataset.preload),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.buttonAlign&&(e.buttonAlign=t.dataset.buttonAlign),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showControls!==void 0&&(e.showControls=t.dataset.showControls==="true"),t.dataset.showInfo!==void 0&&(e.showInfo=t.dataset.showInfo==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.album&&(e.album=t.dataset.album),t.dataset.artwork&&(e.artwork=t.dataset.artwork),t.dataset.waveform&&(e.waveform=t.dataset.waveform),t.dataset.markers)try{e.markers=JSON.parse(t.dataset.markers)}catch(i){console.warn("Invalid markers JSON:",i)}if(t.dataset.playbackRate&&(e.playbackRate=parseFloat(t.dataset.playbackRate)),t.dataset.showPlaybackSpeed!==void 0&&(e.showPlaybackSpeed=t.dataset.showPlaybackSpeed==="true"),t.dataset.playbackRates)try{e.playbackRates=JSON.parse(t.dataset.playbackRates)}catch(i){console.warn("Invalid playbackRates JSON:",i)}return t.dataset.enableMediaSession!==void 0&&(e.enableMediaSession=t.dataset.enableMediaSession==="true"),e}function C(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),i=Math.floor(t%60);return`${e}:${i.toString().padStart(2,"0")}`}function R(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function B(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function P(...t){let e={};for(let i of t)for(let o in i)i[o]!==null&&i[o]!==void 0&&(e[o]=i[o]);return e}function I(t,e){let i;return function(...s){let n=()=>{clearTimeout(i),t(...s)};clearTimeout(i),i=setTimeout(n,e)}}function S(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let i=[];if(e>t.length){let o=(t.length-1)/(e-1);for(let s=0;s<e;s++){let n=s*o,r=Math.floor(n),a=Math.ceil(n),h=n-r;if(a>=t.length)i.push(t[t.length-1]);else if(r===a)i.push(t[r]);else{let l=t[r]*(1-h)+t[a]*h;i.push(l)}}}else{let o=t.length/e;for(let s=0;s<e;s++){let n=Math.floor(s*o),r=Math.floor((s+1)*o),a=0,h=0;for(let l=n;l<=r&&l<t.length;l++)t[l]>a&&(a=t[l]),h++;if(h===0){let l=Math.min(Math.round(s*o),t.length-1);a=t[l]}i.push(a)}}return i}function M(t,e,i,o,s){let n=window.devicePixelRatio||1,r=s.barWidth*n,a=s.barSpacing*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=o*e.width;t.clearRect(0,0,e.width,e.height);for(let y=0;y<l.length;y++){let f=y*(r+a);if(f+r>e.width)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.color,t.fillRect(f,m,r,c)}t.save(),t.beginPath(),t.rect(0,0,p,d),t.clip();for(let y=0;y<l.length;y++){let f=y*(r+a);if(f>p)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.progressColor,t.fillRect(f,m,r,c)}t.restore()}function U(t,e,i,o,s){let n=window.devicePixelRatio||1,r=s.barWidth*n,a=s.barSpacing*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=d/2,y=o*e.width;t.clearRect(0,0,e.width,e.height);for(let f=0;f<l.length;f++){let c=f*(r+a);if(c+r>e.width)break;let m=l[f]*d*.45;t.fillStyle=s.color,t.fillRect(c,p-m,r,m),t.fillRect(c,p,r,m)}t.save(),t.beginPath(),t.rect(0,0,y,d),t.clip();for(let f=0;f<l.length;f++){let c=f*(r+a);if(c>y)break;let m=l[f]*d*.45;t.fillStyle=s.progressColor,t.fillRect(c,p-m,r,m),t.fillRect(c,p,r,m)}t.restore()}function F(t,e,i,o,s){let n=e.width,r=e.height,a=r/2,h=r*.35;t.clearRect(0,0,n,r);let l=(d,p,y=1,f=!1)=>{f&&(t.shadowBlur=12,t.shadowColor=d),t.strokeStyle=d,t.lineWidth=p,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,a);let c=[],m=Math.floor(i.length*y);for(let u=0;u<m;u++){let v=u/(i.length-1)*n,k=i[u],b=Math.sin(u*.1)*k,w=a+b*h;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,k=c[u].y,b=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,k,b,w,c[u+1].x,c[u+1].y)}t.stroke(),f&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,a),t.lineTo(n,a),t.stroke();for(let d=0;d<=10;d++){let p=n/10*d;t.beginPath(),t.moveTo(p,0),t.lineTo(p,r),t.stroke()}l(s.color,2,1,!1),o>0&&l(s.progressColor,3,o,!0)}function W(t,e,i,o,s){let n=window.devicePixelRatio||1,r=(s.barWidth||3)*n,a=(s.barSpacing||1)*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=4*n,y=2*n,f=o*e.width,c=d/2;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let u=m*(r+a);if(u+r>e.width)break;let v=l[m]*d*.9,k=Math.floor(v/(p+y));t.fillStyle=u<f?s.progressColor:s.color;for(let b=0;b<k;b++){let w=b*(p+y);t.fillRect(u,c-w-p,r,p),b>0&&t.fillRect(u,c+w,r,p)}}}function x(t,e,i,o,s){let n=window.devicePixelRatio||1,r=(s.barWidth||2)*n,a=(s.barSpacing||3)*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=Math.max(1.5*n,r/2),y=o*e.width,f=d/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let m=c*(r+a)+r/2;if(m>e.width)break;let u=l[c]*d*.9;t.fillStyle=m<y?s.progressColor:s.color,t.beginPath(),t.arc(m,f-u/2,p,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(m,f+u/2,p,0,Math.PI*2),t.fill()}}function N(t,e,i,o,s){let n=e.width,r=e.height,a=r/2,h=4,l=h/2;if(t.clearRect(0,0,n,r),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,a-h/2),t.lineTo(n-l,a-h/2),t.arc(n-l,a,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,a+h/2),t.arc(l,a,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),o>0){let d=Math.max(l*2,o*n);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,a-h/2),t.lineTo(d-l,a-h/2),t.arc(d-l,a,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,a+h/2),t.arc(l,a,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let p=8,y=d;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(y,a,p,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(y,a,p*.4,0,Math.PI*2),t.fill()}}var Y={bars:M,bar:M,mirror:U,line:F,blocks:W,block:W,dots:x,dot:x,seekbar:N};function D(t,e,i,o,s){(Y[s.waveformStyle]||M)(t,e,i,o,s)}function z(t){try{let e=t.getChannelData(0),i=t.sampleRate,o=V(e,i);if(o.length<2)return 120;let s=[];for(let h=1;h<o.length;h++)s.push((o[h]-o[h-1])/i);let n={};s.forEach(h=>{let l=60/h,d=Math.round(l/3)*3;d>60&&d<200&&(n[d]=(n[d]||0)+1)});let r=0,a=120;for(let[h,l]of Object.entries(n))l>r&&(r=l,a=parseInt(h));return a<70&&n[a*2]?a*=2:a>160&&n[Math.round(a/2)]&&(a=Math.round(a/2)),a-1}catch(e){return console.warn("BPM detection failed:",e),null}}function V(t,e){let s=[],n=0;for(let r=0;r<t.length-2048;r+=1024){let a=0;for(let d=r;d<r+2048;d++)a+=t[d]*t[d];a=a/2048;let h=a-n,l=n*1.8+.01;if(h>l&&a>.01){let d=s[s.length-1]||0,p=e*.15;r-d>p&&s.push(r)}n=a*.8+n*.2}return s}function j(t,e=200){let i=t.length/e,o=~~(i/10)||1,s=t.numberOfChannels,n=[];for(let a=0;a<s;a++){let h=t.getChannelData(a);for(let l=0;l<e;l++){let d=~~(l*i),p=~~(d+i),y=0,f=0;for(let m=d;m<p;m+=o){let u=h[m];u>f&&(f=u),u<y&&(y=u)}let c=Math.max(Math.abs(f),Math.abs(y));(a===0||c>n[l])&&(n[l]=c)}}let r=Math.max(...n);return r>0?n.map(a=>a/r):n}async function E(t,e=200,i=!1){try{let o=new(window.AudioContext||window.webkitAudioContext),n=await(await fetch(t)).arrayBuffer(),r=await o.decodeAudioData(n),a=j(r,e);a=J(a);let h=null;return i&&(h=await z(r)),o.close(),{peaks:a,bpm:h}}catch(o){throw console.error("Failed to generate waveform:",o),o}}function O(t=200){let e=[];for(let i=0;i<t;i++){let o=Math.random()*.5+.3,s=Math.sin(i/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,o+s)))}return e}function J(t,e=.95){let i=Math.max(...t);if(i===0||i>e)return t;let o=e/i;return t.map(s=>s*o)}function G(){let t=document.documentElement,e=document.body;if(t.classList.contains("dark")||t.classList.contains("dark-mode")||t.classList.contains("theme-dark")||t.getAttribute("data-theme")==="dark"||t.getAttribute("data-color-scheme")==="dark"||e.classList.contains("dark")||e.classList.contains("dark-mode")||e.getAttribute("data-theme")==="dark")return"dark";if(t.classList.contains("light")||t.classList.contains("light-mode")||t.classList.contains("theme-light")||t.getAttribute("data-theme")==="light"||t.getAttribute("data-color-scheme")==="light"||e.classList.contains("light")||e.classList.contains("light-mode")||e.getAttribute("data-theme")==="light")return"light";try{let o=getComputedStyle(document.body).backgroundColor.match(/\d+/g);if(o&&o.length>=3){let[s,n,r]=o.map(Number),a=(s*299+n*587+r*114)/1e3;if(a>128)return"light";if(a<128)return"dark"}}catch{}if(window.matchMedia){if(window.matchMedia("(prefers-color-scheme: dark)").matches)return"dark";if(window.matchMedia("(prefers-color-scheme: light)").matches)return"light"}return"dark"}var T={dark:{waveformColor:"rgba(255, 255, 255, 0.3)",progressColor:"rgba(255, 255, 255, 0.9)",buttonColor:"rgba(255, 255, 255, 0.9)",buttonHoverColor:"rgba(255, 255, 255, 1)",textColor:"#ffffff",textSecondaryColor:"rgba(255, 255, 255, 0.6)",backgroundColor:"rgba(255, 255, 255, 0.03)",borderColor:"rgba(255, 255, 255, 0.1)"},light:{waveformColor:"rgba(0, 0, 0, 0.2)",progressColor:"rgba(0, 0, 0, 0.8)",buttonColor:"rgba(0, 0, 0, 0.8)",buttonHoverColor:"rgba(0, 0, 0, 0.9)",textColor:"#333333",textSecondaryColor:"rgba(0, 0, 0, 0.6)",backgroundColor:"rgba(0, 0, 0, 0.02)",borderColor:"rgba(0, 0, 0, 0.1)"}};function H(t){if(t&&T[t])return T[t];let e=G();return T[e]}var $={url:"",height:60,samples:200,preload:"metadata",playbackRate:1,showPlaybackSpeed:!1,playbackRates:[.5,.75,1,1.25,1.5,1.75,2],buttonAlign:"auto",waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:null,waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showControls:!0,showInfo:!0,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,enableMediaSession:!0,markers:[],showMarkers:!0,title:null,subtitle:null,artwork:null,album:"",playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},q={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var g=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,i={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let o=A(this.container);this.options=P($,o,i);let s=H(this.options.colorPreset);for(let[r,a]of Object.entries(s))(this.options[r]===null||this.options[r]===void 0)&&(this.options[r]=a);let n=q[this.options.waveformStyle];n&&(o.barWidth===void 0&&i.barWidth===void 0&&(this.options.barWidth=n.barWidth),o.barSpacing===void 0&&i.barSpacing===void 0&&(this.options.barSpacing=n.barSpacing)),this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||R(this.options.url),t.instances.set(this.id,this),this.init(),setTimeout(()=>{this.container.dispatchEvent(new CustomEvent("waveformplayer:ready",{bubbles:!0,detail:{player:this,url:this.options.url}}))},100)}init(){this.createDOM(),this.createAudio(),this.initPlaybackSpeed(),this.initKeyboardControls(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player";let e=this.options.buttonAlign;e==="auto"&&(this.options.waveformStyle==="bars"?e="bottom":e="center");let i=this.options.showControls?`
|
|
1
|
+
function A(t){let e={};if(t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.preload&&(e.preload=t.dataset.preload),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.buttonAlign&&(e.buttonAlign=t.dataset.buttonAlign),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showControls!==void 0&&(e.showControls=t.dataset.showControls==="true"),t.dataset.showInfo!==void 0&&(e.showInfo=t.dataset.showInfo==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.album&&(e.album=t.dataset.album),t.dataset.artwork&&(e.artwork=t.dataset.artwork),t.dataset.waveform&&(e.waveform=t.dataset.waveform),t.dataset.markers)try{e.markers=JSON.parse(t.dataset.markers)}catch(i){console.warn("Invalid markers JSON:",i)}if(t.dataset.playbackRate&&(e.playbackRate=parseFloat(t.dataset.playbackRate)),t.dataset.showPlaybackSpeed!==void 0&&(e.showPlaybackSpeed=t.dataset.showPlaybackSpeed==="true"),t.dataset.playbackRates)try{e.playbackRates=JSON.parse(t.dataset.playbackRates)}catch(i){console.warn("Invalid playbackRates JSON:",i)}return t.dataset.enableMediaSession!==void 0&&(e.enableMediaSession=t.dataset.enableMediaSession==="true"),e}function S(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),i=Math.floor(t%60);return`${e}:${i.toString().padStart(2,"0")}`}function x(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function B(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function C(...t){let e={};for(let i of t)for(let o in i)i[o]!==null&&i[o]!==void 0&&(e[o]=i[o]);return e}function R(t,e){let i;return function(...s){let a=()=>{clearTimeout(i),t(...s)};clearTimeout(i),i=setTimeout(a,e)}}function P(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let i=[];if(e>t.length){let o=(t.length-1)/(e-1);for(let s=0;s<e;s++){let a=s*o,n=Math.floor(a),r=Math.ceil(a),h=a-n;if(r>=t.length)i.push(t[t.length-1]);else if(n===r)i.push(t[n]);else{let l=t[n]*(1-h)+t[r]*h;i.push(l)}}}else{let o=t.length/e;for(let s=0;s<e;s++){let a=Math.floor(s*o),n=Math.floor((s+1)*o),r=0,h=0;for(let l=a;l<=n&&l<t.length;l++)t[l]>r&&(r=t[l]),h++;if(h===0){let l=Math.min(Math.round(s*o),t.length-1);r=t[l]}i.push(r)}}return i}function E(t,e,i,o,s){let a=window.devicePixelRatio||1,n=s.barWidth*a,r=s.barSpacing*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=o*e.width;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let f=m*(n+r);if(f+n>e.width)break;let c=l[m]*d*.9,y=d-c;t.fillStyle=s.color,t.fillRect(f,y,n,c)}t.save(),t.beginPath(),t.rect(0,0,p,d),t.clip();for(let m=0;m<l.length;m++){let f=m*(n+r);if(f>p)break;let c=l[m]*d*.9,y=d-c;t.fillStyle=s.progressColor,t.fillRect(f,y,n,c)}t.restore()}function U(t,e,i,o,s){let a=window.devicePixelRatio||1,n=s.barWidth*a,r=s.barSpacing*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=d/2,m=o*e.width;t.clearRect(0,0,e.width,e.height);for(let f=0;f<l.length;f++){let c=f*(n+r);if(c+n>e.width)break;let y=l[f]*d*.45;t.fillStyle=s.color,t.fillRect(c,p-y,n,y),t.fillRect(c,p,n,y)}t.save(),t.beginPath(),t.rect(0,0,m,d),t.clip();for(let f=0;f<l.length;f++){let c=f*(n+r);if(c>m)break;let y=l[f]*d*.45;t.fillStyle=s.progressColor,t.fillRect(c,p-y,n,y),t.fillRect(c,p,n,y)}t.restore()}function F(t,e,i,o,s){let a=e.width,n=e.height,r=n/2,h=n*.35;t.clearRect(0,0,a,n);let l=(d,p,m=1,f=!1)=>{f&&(t.shadowBlur=12,t.shadowColor=d),t.strokeStyle=d,t.lineWidth=p,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,r);let c=[],y=Math.floor(i.length*m);for(let u=0;u<y;u++){let v=u/(i.length-1)*a,k=i[u],b=Math.sin(u*.1)*k,w=r+b*h;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,k=c[u].y,b=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,k,b,w,c[u+1].x,c[u+1].y)}t.stroke(),f&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,r),t.lineTo(a,r),t.stroke();for(let d=0;d<=10;d++){let p=a/10*d;t.beginPath(),t.moveTo(p,0),t.lineTo(p,n),t.stroke()}l(s.color,2,1,!1),o>0&&l(s.progressColor,3,o,!0)}function I(t,e,i,o,s){let a=window.devicePixelRatio||1,n=(s.barWidth||3)*a,r=(s.barSpacing||1)*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=4*a,m=2*a,f=o*e.width,c=d/2;t.clearRect(0,0,e.width,e.height);for(let y=0;y<l.length;y++){let u=y*(n+r);if(u+n>e.width)break;let v=l[y]*d*.9,k=Math.floor(v/(p+m));t.fillStyle=u<f?s.progressColor:s.color;for(let b=0;b<k;b++){let w=b*(p+m);t.fillRect(u,c-w-p,n,p),b>0&&t.fillRect(u,c+w,n,p)}}}function D(t,e,i,o,s){let a=window.devicePixelRatio||1,n=(s.barWidth||2)*a,r=(s.barSpacing||3)*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=Math.max(1.5*a,n/2),m=o*e.width,f=d/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let y=c*(n+r)+n/2;if(y>e.width)break;let u=l[c]*d*.9;t.fillStyle=y<m?s.progressColor:s.color,t.beginPath(),t.arc(y,f-u/2,p,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(y,f+u/2,p,0,Math.PI*2),t.fill()}}function N(t,e,i,o,s){let a=e.width,n=e.height,r=n/2,h=4,l=h/2;if(t.clearRect(0,0,a,n),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,r-h/2),t.lineTo(a-l,r-h/2),t.arc(a-l,r,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,r+h/2),t.arc(l,r,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),o>0){let d=Math.max(l*2,o*a);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,r-h/2),t.lineTo(d-l,r-h/2),t.arc(d-l,r,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,r+h/2),t.arc(l,r,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let p=8,m=d;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(m,r,p,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(m,r,p*.4,0,Math.PI*2),t.fill()}}var Y={bars:E,bar:E,mirror:U,line:F,blocks:I,block:I,dots:D,dot:D,seekbar:N};function W(t,e,i,o,s){(Y[s.waveformStyle]||E)(t,e,i,o,s)}function z(t){try{let e=t.getChannelData(0),i=t.sampleRate,o=V(e,i);if(o.length<2)return 120;let s=[];for(let h=1;h<o.length;h++)s.push((o[h]-o[h-1])/i);let a={};s.forEach(h=>{let l=60/h,d=Math.round(l/3)*3;d>60&&d<200&&(a[d]=(a[d]||0)+1)});let n=0,r=120;for(let[h,l]of Object.entries(a))l>n&&(n=l,r=parseInt(h));return r<70&&a[r*2]?r*=2:r>160&&a[Math.round(r/2)]&&(r=Math.round(r/2)),r-1}catch(e){return console.warn("BPM detection failed:",e),null}}function V(t,e){let s=[],a=0;for(let n=0;n<t.length-2048;n+=1024){let r=0;for(let d=n;d<n+2048;d++)r+=t[d]*t[d];r=r/2048;let h=r-a,l=a*1.8+.01;if(h>l&&r>.01){let d=s[s.length-1]||0,p=e*.15;n-d>p&&s.push(n)}a=r*.8+a*.2}return s}function j(t,e=200){let i=t.length/e,o=~~(i/10)||1,s=t.numberOfChannels,a=[];for(let r=0;r<s;r++){let h=t.getChannelData(r);for(let l=0;l<e;l++){let d=~~(l*i),p=~~(d+i),m=0,f=0;for(let y=d;y<p;y+=o){let u=h[y];u>f&&(f=u),u<m&&(m=u)}let c=Math.max(Math.abs(f),Math.abs(m));(r===0||c>a[l])&&(a[l]=c)}}let n=Math.max(...a);return n>0?a.map(r=>r/n):a}async function M(t,e=200,i=!1){try{let o=new(window.AudioContext||window.webkitAudioContext),a=await(await fetch(t)).arrayBuffer(),n=await o.decodeAudioData(a),r=j(n,e);r=J(r);let h=null;return i&&(h=await z(n)),o.close(),{peaks:r,bpm:h}}catch(o){throw console.error("Failed to generate waveform:",o),o}}function O(t=200){let e=[];for(let i=0;i<t;i++){let o=Math.random()*.5+.3,s=Math.sin(i/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,o+s)))}return e}function J(t,e=.95){let i=Math.max(...t);if(i===0||i>e)return t;let o=e/i;return t.map(s=>s*o)}function _(){let t=document.documentElement,e=document.body;if(t.classList.contains("dark")||t.classList.contains("dark-mode")||t.classList.contains("theme-dark")||t.getAttribute("data-theme")==="dark"||t.getAttribute("data-color-scheme")==="dark"||e.classList.contains("dark")||e.classList.contains("dark-mode")||e.getAttribute("data-theme")==="dark")return"dark";if(t.classList.contains("light")||t.classList.contains("light-mode")||t.classList.contains("theme-light")||t.getAttribute("data-theme")==="light"||t.getAttribute("data-color-scheme")==="light"||e.classList.contains("light")||e.classList.contains("light-mode")||e.getAttribute("data-theme")==="light")return"light";try{let o=getComputedStyle(document.body).backgroundColor.match(/\d+/g);if(o&&o.length>=3){let[s,a,n]=o.map(Number),r=(s*299+a*587+n*114)/1e3;if(r>128)return"light";if(r<128)return"dark"}}catch{}if(window.matchMedia){if(window.matchMedia("(prefers-color-scheme: dark)").matches)return"dark";if(window.matchMedia("(prefers-color-scheme: light)").matches)return"light"}return"dark"}var T={dark:{waveformColor:"rgba(255, 255, 255, 0.3)",progressColor:"rgba(255, 255, 255, 0.9)",buttonColor:"rgba(255, 255, 255, 0.9)",buttonHoverColor:"rgba(255, 255, 255, 1)",textColor:"#ffffff",textSecondaryColor:"rgba(255, 255, 255, 0.6)",backgroundColor:"rgba(255, 255, 255, 0.03)",borderColor:"rgba(255, 255, 255, 0.1)"},light:{waveformColor:"rgba(0, 0, 0, 0.2)",progressColor:"rgba(0, 0, 0, 0.8)",buttonColor:"rgba(0, 0, 0, 0.8)",buttonHoverColor:"rgba(0, 0, 0, 0.9)",textColor:"#333333",textSecondaryColor:"rgba(0, 0, 0, 0.6)",backgroundColor:"rgba(0, 0, 0, 0.02)",borderColor:"rgba(0, 0, 0, 0.1)"}};function H(t){if(t&&T[t])return T[t];let e=_();return T[e]}var q={url:"",height:60,samples:200,preload:"metadata",audioMode:"self",playbackRate:1,showPlaybackSpeed:!1,playbackRates:[.5,.75,1,1.25,1.5,1.75,2],buttonAlign:"auto",waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:null,waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showControls:!0,showInfo:!0,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,enableMediaSession:!0,markers:[],showMarkers:!0,title:null,subtitle:null,artwork:null,album:"",playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},$={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var g=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,i={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let o=A(this.container);this.options=C(q,o,i);let s=H(this.options.colorPreset);for(let[n,r]of Object.entries(s))(this.options[n]===null||this.options[n]===void 0)&&(this.options[n]=r);let a=$[this.options.waveformStyle];a&&(o.barWidth===void 0&&i.barWidth===void 0&&(this.options.barWidth=a.barWidth),o.barSpacing===void 0&&i.barSpacing===void 0&&(this.options.barSpacing=a.barSpacing)),this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||x(this.options.url),t.instances.set(this.id,this),this.init(),setTimeout(()=>{this.container.dispatchEvent(new CustomEvent("waveformplayer:ready",{bubbles:!0,detail:{player:this,url:this.options.url}}))},100)}init(){this.createDOM(),this.createAudio(),this.initPlaybackSpeed(),this.initKeyboardControls(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player";let e=this.options.buttonAlign;e==="auto"&&(this.options.waveformStyle==="bars"?e="bottom":e="center");let i=this.options.showControls?`
|
|
2
2
|
<button class="waveform-btn" aria-label="Play/Pause" style="
|
|
3
3
|
border-color: ${this.options.buttonColor};
|
|
4
4
|
color: ${this.options.buttonColor};
|
|
@@ -63,4 +63,4 @@ function A(t){let e={};if(t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&
|
|
|
63
63
|
${o}
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
|
-
`,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.artworkEl=this.container.querySelector(".waveform-artwork"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.markersContainer=this.container.querySelector(".waveform-markers"),this.speedBtn=this.container.querySelector(".speed-btn"),this.speedMenu=this.container.querySelector(".speed-menu"),this.resizeCanvas()}createAudio(){this.audio=new Audio,this.audio.preload=this.options.preload||"metadata",this.audio.crossOrigin="anonymous"}initPlaybackSpeed(){this.options.playbackRate&&this.options.playbackRate!==1&&(this.audio.playbackRate=this.options.playbackRate),this.options.showPlaybackSpeed&&this.initSpeedControls()}initSpeedControls(){let e=this.container.querySelector(".speed-btn"),i=this.container.querySelector(".speed-menu");!e||!i||(e.addEventListener("click",o=>{o.stopPropagation(),i.style.display=i.style.display==="none"?"block":"none"}),document.addEventListener("click",()=>{i.style.display="none"}),i.addEventListener("click",o=>{if(o.stopPropagation(),o.target.classList.contains("speed-option")){let s=parseFloat(o.target.dataset.rate);this.setPlaybackRate(s),i.style.display="none"}}),this.updateSpeedUI())}initKeyboardControls(){this.container.setAttribute("tabindex","-1"),this.container.addEventListener("click",()=>{t.getAllInstances().forEach(e=>{e!==this&&e.container.setAttribute("tabindex","-1")}),this.container.setAttribute("tabindex","0"),this.container.focus()}),this.container.addEventListener("keydown",e=>{if(document.activeElement!==this.container)return;let i=e.key,o=this.audio.currentTime;if(i>="0"&&i<="9"){e.preventDefault(),this.seekToPercent(parseInt(i)/10);return}let s={" ":()=>this.togglePlay(),ArrowLeft:()=>this.seekTo(Math.max(0,o-5)),ArrowRight:()=>this.seekTo(Math.min(this.audio.duration,o+5)),ArrowUp:()=>this.setVolume(Math.min(1,this.audio.volume+.1)),ArrowDown:()=>this.setVolume(Math.max(0,this.audio.volume-.1)),m:()=>this.audio.muted=!this.audio.muted,M:()=>this.audio.muted=!this.audio.muted};s[i]&&(e.preventDefault(),s[i]())})}initMediaSession(){!("mediaSession"in navigator)||!this.options.enableMediaSession||(navigator.mediaSession.metadata=new MediaMetadata({title:this.options.title||"Unknown Track",artist:this.options.subtitle||"",album:this.options.album||"",artwork:this.options.artwork?[{src:this.options.artwork,sizes:"512x512",type:"image/jpeg"}]:[]}),navigator.mediaSession.setActionHandler("play",()=>this.play()),navigator.mediaSession.setActionHandler("pause",()=>this.pause()),navigator.mediaSession.setActionHandler("seekbackward",()=>{this.seekTo(Math.max(0,this.audio.currentTime-10))}),navigator.mediaSession.setActionHandler("seekforward",()=>{this.seekTo(Math.min(this.audio.duration,this.audio.currentTime+10))}),navigator.mediaSession.setActionHandler("seekto",e=>{e.seekTime!==null&&this.seekTo(e.seekTime)}))}bindEvents(){this.playBtn&&this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e)),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),this.resizeHandler=I(()=>this.resizeCanvas(),100),window.addEventListener("resize",this.resizeHandler)}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio.src=e,await new Promise((o,s)=>{let n=()=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",r),o()},r=a=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",r),s(a)};this.audio.addEventListener("loadedmetadata",n),this.audio.addEventListener("error",r)});let i=this.options.title||B(e);if(this.titleEl&&(this.titleEl.textContent=i),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let o=await E(e,this.options.samples,this.options.showBPM);this.waveformData=o.peaks,o.bpm&&(this.detectedBPM=o.bpm,this.updateBPMDisplay())}catch(o){console.warn("Using placeholder waveform:",o),this.waveformData=O(this.options.samples)}this.drawWaveform(),this.renderMarkers(),this.initMediaSession(),this.options.onLoad&&this.options.onLoad(this)}catch(i){console.error("Failed to load audio:",i),this.onError(i)}finally{this.setLoading(!1)}}async loadTrack(e,i=null,o=null,s={}){this.isPlaying&&this.pause(),this.audio.src="",this.audio.load(),this.hasError=!1,this.errorEl&&(this.errorEl.style.display="none"),this.canvas&&(this.canvas.style.opacity="1"),this.playBtn&&(this.playBtn.disabled=!1),this.progress=0,this.waveformData=[],this.options=P(this.options,{url:e,title:i||this.options.title,subtitle:o||this.options.subtitle,...s}),s.preload&&(this.audio.preload=s.preload),this.subtitleEl&&(o?(this.subtitleEl.textContent=o,this.subtitleEl.style.display=""):o===""&&(this.subtitleEl.style.display="none")),s.artwork&&this.artworkEl&&(this.artworkEl.src=s.artwork),this.options.markers=s.markers||[],await this.load(e),this.play().catch(()=>{})}setWaveformData(e){if(typeof e=="string"&&e.trim().endsWith(".json")){fetch(e.trim()).then(i=>i.json()).then(i=>{this.waveformData=Array.isArray(i)?i:i.peaks||[],i.markers&&!this.options.markers?.length&&(this.options.markers=i.markers,this.renderMarkers()),this.drawWaveform()}).catch(()=>{});return}if(typeof e=="string")try{let i=JSON.parse(e);this.waveformData=Array.isArray(i)?i:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||D(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){if(!this.canvas||this.isDestroying)return;let e=window.devicePixelRatio||1,i=this.canvas.parentElement.getBoundingClientRect();this.canvas.width=i.width*e,this.canvas.height=this.options.height*e,this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}renderMarkers(){this.markersContainer&&(this.markersContainer.innerHTML="",!(!this.options.showMarkers||!this.options.markers?.length)&&(!this.audio||!this.audio.duration||this.audio.duration===0||this.options.markers.forEach((e,i)=>{if(e.time>this.audio.duration){console.warn(`Marker "${e.label}" at ${e.time}s exceeds audio duration of ${this.audio.duration}s`);return}let o=e.time/this.audio.duration*100,s=document.createElement("button");s.className="waveform-marker",s.style.left=`${o}%`,s.style.backgroundColor=e.color||"rgba(255, 255, 255, 0.5)",s.setAttribute("aria-label",e.label),s.setAttribute("data-time",e.time);let n=document.createElement("span");n.className="waveform-marker-tooltip",n.textContent=e.label,s.appendChild(n),s.addEventListener("click",r=>{r.stopPropagation(),this.seekTo(e.time),this.options.playOnSeek&&!this.isPlaying&&this.play()}),this.markersContainer.appendChild(s)})))}handleCanvasClick(e){if(!this.audio.duration)return;let i=this.canvas.getBoundingClientRect(),o=e.clientX-i.left,s=Math.max(0,Math.min(1,o/i.width));this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.isDestroying||(this.totalTimeEl&&(this.totalTimeEl.textContent=C(this.audio.duration)),this.renderMarkers())}onPlay(){if(!this.isDestroying){if(this.isPlaying=!0,this.playBtn){this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),i&&(i.style.display="flex")}this.startSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)}}onPause(){if(!this.isDestroying){if(this.isPlaying=!1,this.playBtn){this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),i&&(i.style.display="none")}this.stopSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this)}}onEnded(){this.isDestroying||(this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.container.dispatchEvent(new CustomEvent("waveformplayer:ended",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.onPause(),this.options.onEnd&&this.options.onEnd(this))}onError(e){this.isDestroying||(console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this))}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=C(this.audio.currentTime)),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:this.audio.currentTime,duration:this.audio.duration,url:this.options.url}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}updateSpeedUI(){let e=this.container.querySelector(".speed-value");if(e){let i=this.audio.playbackRate;e.textContent=i===1?"1x":`${i}x`}this.container.querySelectorAll(".speed-option").forEach(i=>{i.classList.toggle("active",parseFloat(i.dataset.rate)===this.audio.playbackRate)})}play(){return this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),t.currentlyPlaying=this,this.audio.play()}pause(){t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio.pause()}togglePlay(){this.isPlaying?this.pause():this.play()}seekTo(e){this.audio&&this.audio.duration&&(this.audio.currentTime=Math.max(0,Math.min(e,this.audio.duration)),this.updateProgress())}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}setPlaybackRate(e){if(!this.audio)return;let i=Math.max(.5,Math.min(2,e));this.audio.playbackRate=i,this.options.playbackRate=i,this.updateSpeedUI()}destroy(){this.isDestroying=!0,this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.resizeHandler&&(window.removeEventListener("resize",this.resizeHandler),this.resizeHandler=null),t.instances.delete(this.id),t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio&&(this.audio.pause(),this.audio.src="",this.audio.load(),this.audio=null),this.container.innerHTML="",this.canvas=null,this.ctx=null,this.playBtn=null,this.waveformData=[]}static getInstance(e){if(typeof e=="string"){let i=this.instances.get(e);if(i)return i;let o=document.getElementById(e);if(o)return Array.from(this.instances.values()).find(s=>s.container===o)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(i=>i.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,i=200){try{return(await E(e,i)).peaks}catch(o){throw console.error("Failed to generate waveform:",o),o}}};function L(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new g(e),e.dataset.waveformInitialized="true"}catch(i){console.error("Failed to initialize WaveformPlayer:",i,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L());g.init=L;typeof window<"u"&&(window.WaveformPlayer=g);var lt=g;export{g as WaveformPlayer,lt as default};
|
|
66
|
+
`,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.artworkEl=this.container.querySelector(".waveform-artwork"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.markersContainer=this.container.querySelector(".waveform-markers"),this.speedBtn=this.container.querySelector(".speed-btn"),this.speedMenu=this.container.querySelector(".speed-menu"),this.resizeCanvas()}createAudio(){if(this.options.audioMode==="external"){this.audio=null;return}this.audio=new Audio,this.audio.preload=this.options.preload||"metadata",this.audio.crossOrigin="anonymous"}initPlaybackSpeed(){this.audio&&this.options.playbackRate&&this.options.playbackRate!==1&&(this.audio.playbackRate=this.options.playbackRate),this.options.showPlaybackSpeed&&this.initSpeedControls()}initSpeedControls(){let e=this.container.querySelector(".speed-btn"),i=this.container.querySelector(".speed-menu");!e||!i||(e.addEventListener("click",o=>{o.stopPropagation(),i.style.display=i.style.display==="none"?"block":"none"}),document.addEventListener("click",()=>{i.style.display="none"}),i.addEventListener("click",o=>{if(o.stopPropagation(),o.target.classList.contains("speed-option")){let s=parseFloat(o.target.dataset.rate);this.setPlaybackRate(s),i.style.display="none"}}),this.updateSpeedUI())}initKeyboardControls(){this.container.setAttribute("tabindex","-1"),this.container.addEventListener("click",()=>{t.getAllInstances().forEach(e=>{e!==this&&e.container.setAttribute("tabindex","-1")}),this.container.setAttribute("tabindex","0"),this.container.focus()}),this.container.addEventListener("keydown",e=>{if(document.activeElement!==this.container)return;let i=e.key,o=!!this.audio,s=o?this.audio.currentTime:0;if(o&&i>="0"&&i<="9"){e.preventDefault(),this.seekToPercent(parseInt(i)/10);return}let a={" ":()=>this.togglePlay()};o&&(a.ArrowLeft=()=>this.seekTo(Math.max(0,s-5)),a.ArrowRight=()=>this.seekTo(Math.min(this.audio.duration,s+5)),a.ArrowUp=()=>this.setVolume(Math.min(1,this.audio.volume+.1)),a.ArrowDown=()=>this.setVolume(Math.max(0,this.audio.volume-.1)),a.m=a.M=()=>this.audio.muted=!this.audio.muted),a[i]&&(e.preventDefault(),a[i]())})}initMediaSession(){!("mediaSession"in navigator)||!this.options.enableMediaSession||this.audio&&(navigator.mediaSession.metadata=new MediaMetadata({title:this.options.title||"Unknown Track",artist:this.options.subtitle||"",album:this.options.album||"",artwork:this.options.artwork?[{src:this.options.artwork,sizes:"512x512",type:"image/jpeg"}]:[]}),navigator.mediaSession.setActionHandler("play",()=>this.play()),navigator.mediaSession.setActionHandler("pause",()=>this.pause()),navigator.mediaSession.setActionHandler("seekbackward",()=>{this.seekTo(Math.max(0,this.audio.currentTime-10))}),navigator.mediaSession.setActionHandler("seekforward",()=>{this.seekTo(Math.min(this.audio.duration,this.audio.currentTime+10))}),navigator.mediaSession.setActionHandler("seekto",e=>{e.seekTime!==null&&this.seekTo(e.seekTime)}))}bindEvents(){this.playBtn&&this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio&&(this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e))),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),this.resizeHandler=R(()=>this.resizeCanvas(),100),window.addEventListener("resize",this.resizeHandler)}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio&&(this.audio.src=e,await new Promise((o,s)=>{let a=()=>{this.audio.removeEventListener("loadedmetadata",a),this.audio.removeEventListener("error",n),o()},n=r=>{this.audio.removeEventListener("loadedmetadata",a),this.audio.removeEventListener("error",n),s(r)};this.audio.addEventListener("loadedmetadata",a),this.audio.addEventListener("error",n)}));let i=this.options.title||B(e);if(this.titleEl&&(this.titleEl.textContent=i),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let o=await M(e,this.options.samples,this.options.showBPM);this.waveformData=o.peaks,o.bpm&&(this.detectedBPM=o.bpm,this.updateBPMDisplay())}catch(o){console.warn("Using placeholder waveform:",o),this.waveformData=O(this.options.samples)}this.drawWaveform(),this.renderMarkers(),this.initMediaSession(),this.options.onLoad&&this.options.onLoad(this)}catch(i){console.error("Failed to load audio:",i),this.onError(i)}finally{this.setLoading(!1)}}async loadTrack(e,i=null,o=null,s={}){this.isPlaying&&this.pause(),this.audio&&(this.audio.src="",this.audio.load()),this.hasError=!1,this.errorEl&&(this.errorEl.style.display="none"),this.canvas&&(this.canvas.style.opacity="1"),this.playBtn&&(this.playBtn.disabled=!1),this.progress=0,this.waveformData=[],this.options=C(this.options,{url:e,title:i||this.options.title,subtitle:o||this.options.subtitle,...s}),s.preload&&this.audio&&(this.audio.preload=s.preload),this.subtitleEl&&(o?(this.subtitleEl.textContent=o,this.subtitleEl.style.display=""):o===""&&(this.subtitleEl.style.display="none")),s.artwork&&this.artworkEl&&(this.artworkEl.src=s.artwork),this.options.markers=s.markers||[],await this.load(e),this.play().catch(()=>{})}setWaveformData(e){if(typeof e=="string"&&e.trim().endsWith(".json")){fetch(e.trim()).then(i=>i.json()).then(i=>{this.waveformData=Array.isArray(i)?i:i.peaks||[],i.markers&&!this.options.markers?.length&&(this.options.markers=i.markers,this.renderMarkers()),this.drawWaveform()}).catch(()=>{});return}if(typeof e=="string")try{let i=JSON.parse(e);this.waveformData=Array.isArray(i)?i:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||W(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){if(!this.canvas||this.isDestroying)return;let e=window.devicePixelRatio||1,i=this.canvas.parentElement.getBoundingClientRect();this.canvas.width=i.width*e,this.canvas.height=this.options.height*e,this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}renderMarkers(){this.markersContainer&&(this.markersContainer.innerHTML="",!(!this.options.showMarkers||!this.options.markers?.length)&&(!this.audio||!this.audio.duration||this.audio.duration===0||this.options.markers.forEach((e,i)=>{if(e.time>this.audio.duration){console.warn(`Marker "${e.label}" at ${e.time}s exceeds audio duration of ${this.audio.duration}s`);return}let o=e.time/this.audio.duration*100,s=document.createElement("button");s.className="waveform-marker",s.style.left=`${o}%`,s.style.backgroundColor=e.color||"rgba(255, 255, 255, 0.5)",s.setAttribute("aria-label",e.label),s.setAttribute("data-time",e.time);let a=document.createElement("span");a.className="waveform-marker-tooltip",a.textContent=e.label,s.appendChild(a),s.addEventListener("click",n=>{n.stopPropagation(),this.seekTo(e.time),this.options.playOnSeek&&!this.isPlaying&&this.play()}),this.markersContainer.appendChild(s)})))}handleCanvasClick(e){let i=this.canvas.getBoundingClientRect(),o=e.clientX-i.left,s=Math.max(0,Math.min(1,o/i.width));if(this.options.audioMode==="external"){let a=new CustomEvent("waveformplayer:request-seek",{bubbles:!0,cancelable:!0,detail:{...this._buildTrackDetail(),percent:s}});this.container.dispatchEvent(a),a.defaultPrevented||(this.progress=s,this.drawWaveform?.());return}!this.audio||!this.audio.duration||this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.isDestroying||(this.totalTimeEl&&(this.totalTimeEl.textContent=S(this.audio.duration)),this.renderMarkers())}onPlay(){if(!this.isDestroying){if(this.isPlaying=!0,this.playBtn){this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),i&&(i.style.display="flex")}this.startSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)}}onPause(){if(!this.isDestroying){if(this.isPlaying=!1,this.playBtn){this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),i&&(i.style.display="none")}this.stopSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this)}}onEnded(){this.isDestroying||(this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.container.dispatchEvent(new CustomEvent("waveformplayer:ended",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.onPause(),this.options.onEnd&&this.options.onEnd(this))}onError(e){this.isDestroying||(console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this))}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio||!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=S(this.audio.currentTime)),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:this.audio.currentTime,duration:this.audio.duration,url:this.options.url}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}updateSpeedUI(){let e=this.container.querySelector(".speed-value");if(e){let i=this.audio.playbackRate;e.textContent=i===1?"1x":`${i}x`}this.container.querySelectorAll(".speed-option").forEach(i=>{i.classList.toggle("active",parseFloat(i.dataset.rate)===this.audio.playbackRate)})}play(){if(this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),this.options.audioMode==="external"){let e=new CustomEvent("waveformplayer:request-play",{bubbles:!0,cancelable:!0,detail:this._buildTrackDetail()});this.container.dispatchEvent(e),e.defaultPrevented||(t.currentlyPlaying=this);return}return t.currentlyPlaying=this,this.audio.play()}pause(){if(t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.options.audioMode==="external"){this.container.dispatchEvent(new CustomEvent("waveformplayer:request-pause",{bubbles:!0,cancelable:!0,detail:this._buildTrackDetail()}));return}this.audio.pause()}_buildTrackDetail(){return{url:this.options.url,title:this.options.title,subtitle:this.options.subtitle,artist:this.options.artist,artwork:this.options.artwork,id:this.id,player:this}}setPlayingState(e){let i=this.isPlaying;if(this.isPlaying=!!e,this.playBtn){this.playBtn.classList.toggle("playing",this.isPlaying);let o=this.playBtn.querySelector(".waveform-icon-play"),s=this.playBtn.querySelector(".waveform-icon-pause");o&&(o.style.display=this.isPlaying?"none":"flex"),s&&(s.style.display=this.isPlaying?"flex":"none")}this.isPlaying&&!i?(this.startSmoothUpdate?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)):!this.isPlaying&&i&&(this.stopSmoothUpdate?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this))}setProgress(e,i){!i||i<=0||(this.progress=Math.max(0,Math.min(1,e/i)),this.currentTimeEl&&(this.currentTimeEl.textContent=S(e)),this.totalTimeEl&&(!this.totalTimeEl.dataset._extSet||this._extDuration!==i)&&(this.totalTimeEl.textContent=S(i),this.totalTimeEl.dataset._extSet="1",this._extDuration=i),this.drawWaveform?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:e,duration:i,progress:this.progress}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this,e,i))}togglePlay(){this.isPlaying?this.pause():this.play()}seekTo(e){this.audio&&this.audio.duration&&(this.audio.currentTime=Math.max(0,Math.min(e,this.audio.duration)),this.updateProgress())}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}setPlaybackRate(e){if(!this.audio)return;let i=Math.max(.5,Math.min(2,e));this.audio.playbackRate=i,this.options.playbackRate=i,this.updateSpeedUI()}destroy(){this.isDestroying=!0,this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.resizeHandler&&(window.removeEventListener("resize",this.resizeHandler),this.resizeHandler=null),t.instances.delete(this.id),t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio&&(this.audio.pause(),this.audio.src="",this.audio.load(),this.audio=null),this.container.innerHTML="",this.canvas=null,this.ctx=null,this.playBtn=null,this.waveformData=[]}static getInstance(e){if(typeof e=="string"){let i=this.instances.get(e);if(i)return i;let o=document.getElementById(e);if(o)return Array.from(this.instances.values()).find(s=>s.container===o)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(i=>i.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,i=200){try{return(await M(e,i)).peaks}catch(o){throw console.error("Failed to generate waveform:",o),o}}};function L(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new g(e),e.dataset.waveformInitialized="true"}catch(i){console.error("Failed to initialize WaveformPlayer:",i,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L());g.init=L;typeof window<"u"&&(window.WaveformPlayer=g);var lt=g;export{g as WaveformPlayer,lt as default};
|
package/dist/waveform-player.js
CHANGED
|
@@ -577,6 +577,12 @@
|
|
|
577
577
|
height: 60,
|
|
578
578
|
samples: 200,
|
|
579
579
|
preload: "metadata",
|
|
580
|
+
// Audio mode — 'self' = player owns the <audio> element (default, current
|
|
581
|
+
// behavior). 'external' = player is a visualization-only surface; no audio
|
|
582
|
+
// element is created, play() dispatches `waveformplayer:request-play`
|
|
583
|
+
// instead of calling audio.play(), and setPlayingState/setProgress are
|
|
584
|
+
// expected to be driven by an external controller (e.g. WaveformBar).
|
|
585
|
+
audioMode: "self",
|
|
580
586
|
// Playback
|
|
581
587
|
playbackRate: 1,
|
|
582
588
|
showPlaybackSpeed: false,
|
|
@@ -822,8 +828,18 @@
|
|
|
822
828
|
/**
|
|
823
829
|
* Create audio element
|
|
824
830
|
* @private
|
|
831
|
+
*
|
|
832
|
+
* No-op in `audioMode: 'external'` — the player has no audio of its
|
|
833
|
+
* own; an external controller (e.g. WaveformBar) owns playback and
|
|
834
|
+
* pushes state in via setPlayingState() / setProgress(). The
|
|
835
|
+
* `this.audio` field stays null in that mode; downstream code must
|
|
836
|
+
* null-check it.
|
|
825
837
|
*/
|
|
826
838
|
createAudio() {
|
|
839
|
+
if (this.options.audioMode === "external") {
|
|
840
|
+
this.audio = null;
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
827
843
|
this.audio = new Audio();
|
|
828
844
|
this.audio.preload = this.options.preload || "metadata";
|
|
829
845
|
this.audio.crossOrigin = "anonymous";
|
|
@@ -836,7 +852,7 @@
|
|
|
836
852
|
* @private
|
|
837
853
|
*/
|
|
838
854
|
initPlaybackSpeed() {
|
|
839
|
-
if (this.options.playbackRate && this.options.playbackRate !== 1) {
|
|
855
|
+
if (this.audio && this.options.playbackRate && this.options.playbackRate !== 1) {
|
|
840
856
|
this.audio.playbackRate = this.options.playbackRate;
|
|
841
857
|
}
|
|
842
858
|
if (this.options.showPlaybackSpeed) {
|
|
@@ -886,21 +902,23 @@
|
|
|
886
902
|
this.container.addEventListener("keydown", (e) => {
|
|
887
903
|
if (document.activeElement !== this.container) return;
|
|
888
904
|
const key = e.key;
|
|
889
|
-
const
|
|
890
|
-
|
|
905
|
+
const hasAudio = !!this.audio;
|
|
906
|
+
const currentTime = hasAudio ? this.audio.currentTime : 0;
|
|
907
|
+
if (hasAudio && key >= "0" && key <= "9") {
|
|
891
908
|
e.preventDefault();
|
|
892
909
|
this.seekToPercent(parseInt(key) / 10);
|
|
893
910
|
return;
|
|
894
911
|
}
|
|
895
912
|
const actions = {
|
|
896
|
-
" ": () => this.togglePlay()
|
|
897
|
-
"ArrowLeft": () => this.seekTo(Math.max(0, currentTime - 5)),
|
|
898
|
-
"ArrowRight": () => this.seekTo(Math.min(this.audio.duration, currentTime + 5)),
|
|
899
|
-
"ArrowUp": () => this.setVolume(Math.min(1, this.audio.volume + 0.1)),
|
|
900
|
-
"ArrowDown": () => this.setVolume(Math.max(0, this.audio.volume - 0.1)),
|
|
901
|
-
"m": () => this.audio.muted = !this.audio.muted,
|
|
902
|
-
"M": () => this.audio.muted = !this.audio.muted
|
|
913
|
+
" ": () => this.togglePlay()
|
|
903
914
|
};
|
|
915
|
+
if (hasAudio) {
|
|
916
|
+
actions["ArrowLeft"] = () => this.seekTo(Math.max(0, currentTime - 5));
|
|
917
|
+
actions["ArrowRight"] = () => this.seekTo(Math.min(this.audio.duration, currentTime + 5));
|
|
918
|
+
actions["ArrowUp"] = () => this.setVolume(Math.min(1, this.audio.volume + 0.1));
|
|
919
|
+
actions["ArrowDown"] = () => this.setVolume(Math.max(0, this.audio.volume - 0.1));
|
|
920
|
+
actions["m"] = actions["M"] = () => this.audio.muted = !this.audio.muted;
|
|
921
|
+
}
|
|
904
922
|
if (actions[key]) {
|
|
905
923
|
e.preventDefault();
|
|
906
924
|
actions[key]();
|
|
@@ -913,6 +931,7 @@
|
|
|
913
931
|
*/
|
|
914
932
|
initMediaSession() {
|
|
915
933
|
if (!("mediaSession" in navigator) || !this.options.enableMediaSession) return;
|
|
934
|
+
if (!this.audio) return;
|
|
916
935
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
917
936
|
title: this.options.title || "Unknown Track",
|
|
918
937
|
artist: this.options.subtitle || "",
|
|
@@ -946,13 +965,15 @@
|
|
|
946
965
|
if (this.playBtn) {
|
|
947
966
|
this.playBtn.addEventListener("click", () => this.togglePlay());
|
|
948
967
|
}
|
|
949
|
-
this.audio
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
968
|
+
if (this.audio) {
|
|
969
|
+
this.audio.addEventListener("loadstart", () => this.setLoading(true));
|
|
970
|
+
this.audio.addEventListener("loadedmetadata", () => this.onMetadataLoaded());
|
|
971
|
+
this.audio.addEventListener("canplay", () => this.setLoading(false));
|
|
972
|
+
this.audio.addEventListener("play", () => this.onPlay());
|
|
973
|
+
this.audio.addEventListener("pause", () => this.onPause());
|
|
974
|
+
this.audio.addEventListener("ended", () => this.onEnded());
|
|
975
|
+
this.audio.addEventListener("error", (e) => this.onError(e));
|
|
976
|
+
}
|
|
956
977
|
this.canvas.addEventListener("click", (e) => this.handleCanvasClick(e));
|
|
957
978
|
this.resizeHandler = debounce(() => this.resizeCanvas(), 100);
|
|
958
979
|
window.addEventListener("resize", this.resizeHandler);
|
|
@@ -984,21 +1005,23 @@
|
|
|
984
1005
|
this.setLoading(true);
|
|
985
1006
|
this.progress = 0;
|
|
986
1007
|
this.hasError = false;
|
|
987
|
-
this.audio
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1008
|
+
if (this.audio) {
|
|
1009
|
+
this.audio.src = url;
|
|
1010
|
+
await new Promise((resolve, reject) => {
|
|
1011
|
+
const metadataHandler = () => {
|
|
1012
|
+
this.audio.removeEventListener("loadedmetadata", metadataHandler);
|
|
1013
|
+
this.audio.removeEventListener("error", errorHandler);
|
|
1014
|
+
resolve();
|
|
1015
|
+
};
|
|
1016
|
+
const errorHandler = (e) => {
|
|
1017
|
+
this.audio.removeEventListener("loadedmetadata", metadataHandler);
|
|
1018
|
+
this.audio.removeEventListener("error", errorHandler);
|
|
1019
|
+
reject(e);
|
|
1020
|
+
};
|
|
1021
|
+
this.audio.addEventListener("loadedmetadata", metadataHandler);
|
|
1022
|
+
this.audio.addEventListener("error", errorHandler);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1002
1025
|
const title = this.options.title || extractTitleFromUrl(url);
|
|
1003
1026
|
if (this.titleEl) {
|
|
1004
1027
|
this.titleEl.textContent = title;
|
|
@@ -1043,8 +1066,10 @@
|
|
|
1043
1066
|
if (this.isPlaying) {
|
|
1044
1067
|
this.pause();
|
|
1045
1068
|
}
|
|
1046
|
-
this.audio
|
|
1047
|
-
|
|
1069
|
+
if (this.audio) {
|
|
1070
|
+
this.audio.src = "";
|
|
1071
|
+
this.audio.load();
|
|
1072
|
+
}
|
|
1048
1073
|
this.hasError = false;
|
|
1049
1074
|
if (this.errorEl) {
|
|
1050
1075
|
this.errorEl.style.display = "none";
|
|
@@ -1063,7 +1088,7 @@
|
|
|
1063
1088
|
subtitle: subtitle || this.options.subtitle,
|
|
1064
1089
|
...options
|
|
1065
1090
|
});
|
|
1066
|
-
if (options.preload) {
|
|
1091
|
+
if (options.preload && this.audio) {
|
|
1067
1092
|
this.audio.preload = options.preload;
|
|
1068
1093
|
}
|
|
1069
1094
|
if (this.subtitleEl) {
|
|
@@ -1187,10 +1212,23 @@
|
|
|
1187
1212
|
* @private
|
|
1188
1213
|
*/
|
|
1189
1214
|
handleCanvasClick(event) {
|
|
1190
|
-
if (!this.audio.duration) return;
|
|
1191
1215
|
const rect = this.canvas.getBoundingClientRect();
|
|
1192
1216
|
const x = event.clientX - rect.left;
|
|
1193
1217
|
const targetPercent = Math.max(0, Math.min(1, x / rect.width));
|
|
1218
|
+
if (this.options.audioMode === "external") {
|
|
1219
|
+
const evt = new CustomEvent("waveformplayer:request-seek", {
|
|
1220
|
+
bubbles: true,
|
|
1221
|
+
cancelable: true,
|
|
1222
|
+
detail: { ...this._buildTrackDetail(), percent: targetPercent }
|
|
1223
|
+
});
|
|
1224
|
+
this.container.dispatchEvent(evt);
|
|
1225
|
+
if (!evt.defaultPrevented) {
|
|
1226
|
+
this.progress = targetPercent;
|
|
1227
|
+
this.drawWaveform?.();
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (!this.audio || !this.audio.duration) return;
|
|
1194
1232
|
this.seekToPercent(targetPercent);
|
|
1195
1233
|
}
|
|
1196
1234
|
/**
|
|
@@ -1313,7 +1351,7 @@
|
|
|
1313
1351
|
startSmoothUpdate() {
|
|
1314
1352
|
this.stopSmoothUpdate();
|
|
1315
1353
|
const update = () => {
|
|
1316
|
-
if (this.isPlaying && this.audio.duration) {
|
|
1354
|
+
if (this.isPlaying && this.audio && this.audio.duration) {
|
|
1317
1355
|
this.updateProgress();
|
|
1318
1356
|
this.updateTimer = requestAnimationFrame(update);
|
|
1319
1357
|
}
|
|
@@ -1335,7 +1373,7 @@
|
|
|
1335
1373
|
* @private
|
|
1336
1374
|
*/
|
|
1337
1375
|
updateProgress() {
|
|
1338
|
-
if (!this.audio.duration) return;
|
|
1376
|
+
if (!this.audio || !this.audio.duration) return;
|
|
1339
1377
|
const newProgress = this.audio.currentTime / this.audio.duration;
|
|
1340
1378
|
if (Math.abs(newProgress - this.progress) > 1e-3) {
|
|
1341
1379
|
this.progress = newProgress;
|
|
@@ -1388,25 +1426,137 @@
|
|
|
1388
1426
|
// Public API
|
|
1389
1427
|
// ============================================
|
|
1390
1428
|
/**
|
|
1391
|
-
* Play audio
|
|
1392
|
-
*
|
|
1429
|
+
* Play audio.
|
|
1430
|
+
*
|
|
1431
|
+
* In `audioMode: 'self'` (default): calls the underlying <audio>
|
|
1432
|
+
* element's play(). Returns the promise from HTMLMediaElement.play().
|
|
1433
|
+
*
|
|
1434
|
+
* In `audioMode: 'external'`: dispatches a cancelable
|
|
1435
|
+
* `waveformplayer:request-play` event with the track metadata and
|
|
1436
|
+
* does NOT touch any audio element. Returns `undefined`. An external
|
|
1437
|
+
* controller (e.g. WaveformBar) listens for this event and starts
|
|
1438
|
+
* playback on its own audio source, then pushes state back via
|
|
1439
|
+
* setPlayingState() / setProgress(). Calling preventDefault() on
|
|
1440
|
+
* the event lets the controller veto the play (state is unchanged).
|
|
1441
|
+
*
|
|
1442
|
+
* @return {Promise|undefined}
|
|
1393
1443
|
*/
|
|
1394
1444
|
play() {
|
|
1395
1445
|
if (this.options.singlePlay && _WaveformPlayer.currentlyPlaying && _WaveformPlayer.currentlyPlaying !== this) {
|
|
1396
1446
|
_WaveformPlayer.currentlyPlaying.pause();
|
|
1397
1447
|
}
|
|
1448
|
+
if (this.options.audioMode === "external") {
|
|
1449
|
+
const evt = new CustomEvent("waveformplayer:request-play", {
|
|
1450
|
+
bubbles: true,
|
|
1451
|
+
cancelable: true,
|
|
1452
|
+
detail: this._buildTrackDetail()
|
|
1453
|
+
});
|
|
1454
|
+
this.container.dispatchEvent(evt);
|
|
1455
|
+
if (!evt.defaultPrevented) {
|
|
1456
|
+
_WaveformPlayer.currentlyPlaying = this;
|
|
1457
|
+
}
|
|
1458
|
+
return void 0;
|
|
1459
|
+
}
|
|
1398
1460
|
_WaveformPlayer.currentlyPlaying = this;
|
|
1399
1461
|
return this.audio.play();
|
|
1400
1462
|
}
|
|
1401
1463
|
/**
|
|
1402
|
-
* Pause audio
|
|
1464
|
+
* Pause audio.
|
|
1465
|
+
*
|
|
1466
|
+
* In `audioMode: 'external'`, dispatches `waveformplayer:request-pause`
|
|
1467
|
+
* (cancelable) and does NOT touch any audio element. See play().
|
|
1403
1468
|
*/
|
|
1404
1469
|
pause() {
|
|
1405
1470
|
if (_WaveformPlayer.currentlyPlaying === this) {
|
|
1406
1471
|
_WaveformPlayer.currentlyPlaying = null;
|
|
1407
1472
|
}
|
|
1473
|
+
if (this.options.audioMode === "external") {
|
|
1474
|
+
this.container.dispatchEvent(new CustomEvent("waveformplayer:request-pause", {
|
|
1475
|
+
bubbles: true,
|
|
1476
|
+
cancelable: true,
|
|
1477
|
+
detail: this._buildTrackDetail()
|
|
1478
|
+
}));
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1408
1481
|
this.audio.pause();
|
|
1409
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Build the track detail object dispatched by request-play /
|
|
1485
|
+
* request-pause events in external audio mode. Mirrors the shape
|
|
1486
|
+
* WaveformBar.play() accepts so a controller can forward it
|
|
1487
|
+
* directly: `WaveformBar.play(event.detail)`.
|
|
1488
|
+
*
|
|
1489
|
+
* @private
|
|
1490
|
+
* @return {{url:string,title:?string,subtitle:?string,artist:?string,artwork:?string,player:WaveformPlayer}}
|
|
1491
|
+
*/
|
|
1492
|
+
_buildTrackDetail() {
|
|
1493
|
+
return {
|
|
1494
|
+
url: this.options.url,
|
|
1495
|
+
title: this.options.title,
|
|
1496
|
+
subtitle: this.options.subtitle,
|
|
1497
|
+
artist: this.options.artist,
|
|
1498
|
+
artwork: this.options.artwork,
|
|
1499
|
+
id: this.id,
|
|
1500
|
+
player: this
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* External-mode state pump: flip the play/pause visual state without
|
|
1505
|
+
* touching audio. Mirrors what onPlay()/onPause() do but skips the
|
|
1506
|
+
* audio-element interactions. Safe to call repeatedly — idempotent.
|
|
1507
|
+
*
|
|
1508
|
+
* @param {boolean} playing
|
|
1509
|
+
*/
|
|
1510
|
+
setPlayingState(playing) {
|
|
1511
|
+
const wasPlaying = this.isPlaying;
|
|
1512
|
+
this.isPlaying = !!playing;
|
|
1513
|
+
if (this.playBtn) {
|
|
1514
|
+
this.playBtn.classList.toggle("playing", this.isPlaying);
|
|
1515
|
+
const playIcon = this.playBtn.querySelector(".waveform-icon-play");
|
|
1516
|
+
const pauseIcon = this.playBtn.querySelector(".waveform-icon-pause");
|
|
1517
|
+
if (playIcon) playIcon.style.display = this.isPlaying ? "none" : "flex";
|
|
1518
|
+
if (pauseIcon) pauseIcon.style.display = this.isPlaying ? "flex" : "none";
|
|
1519
|
+
}
|
|
1520
|
+
if (this.isPlaying && !wasPlaying) {
|
|
1521
|
+
this.startSmoothUpdate?.();
|
|
1522
|
+
this.container.dispatchEvent(new CustomEvent("waveformplayer:play", {
|
|
1523
|
+
bubbles: true,
|
|
1524
|
+
detail: { player: this, url: this.options.url }
|
|
1525
|
+
}));
|
|
1526
|
+
if (this.options.onPlay) this.options.onPlay(this);
|
|
1527
|
+
} else if (!this.isPlaying && wasPlaying) {
|
|
1528
|
+
this.stopSmoothUpdate?.();
|
|
1529
|
+
this.container.dispatchEvent(new CustomEvent("waveformplayer:pause", {
|
|
1530
|
+
bubbles: true,
|
|
1531
|
+
detail: { player: this, url: this.options.url }
|
|
1532
|
+
}));
|
|
1533
|
+
if (this.options.onPause) this.options.onPause(this);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* External-mode state pump: update the visualization's progress
|
|
1538
|
+
* from an external clock (e.g. WaveformBar's audio element's
|
|
1539
|
+
* timeupdate). Drives the canvas redraw + the time displays.
|
|
1540
|
+
*
|
|
1541
|
+
* @param {number} currentTime Current playback position in seconds.
|
|
1542
|
+
* @param {number} duration Total track duration in seconds.
|
|
1543
|
+
*/
|
|
1544
|
+
setProgress(currentTime, duration) {
|
|
1545
|
+
if (!duration || duration <= 0) return;
|
|
1546
|
+
this.progress = Math.max(0, Math.min(1, currentTime / duration));
|
|
1547
|
+
if (this.currentTimeEl) this.currentTimeEl.textContent = formatTime(currentTime);
|
|
1548
|
+
if (this.totalTimeEl && (!this.totalTimeEl.dataset._extSet || this._extDuration !== duration)) {
|
|
1549
|
+
this.totalTimeEl.textContent = formatTime(duration);
|
|
1550
|
+
this.totalTimeEl.dataset._extSet = "1";
|
|
1551
|
+
this._extDuration = duration;
|
|
1552
|
+
}
|
|
1553
|
+
this.drawWaveform?.();
|
|
1554
|
+
this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate", {
|
|
1555
|
+
bubbles: true,
|
|
1556
|
+
detail: { player: this, currentTime, duration, progress: this.progress }
|
|
1557
|
+
}));
|
|
1558
|
+
if (this.options.onTimeUpdate) this.options.onTimeUpdate(this, currentTime, duration);
|
|
1559
|
+
}
|
|
1410
1560
|
/**
|
|
1411
1561
|
* Toggle play/pause
|
|
1412
1562
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(()=>{function A(t){let e={};if(t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.preload&&(e.preload=t.dataset.preload),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.buttonAlign&&(e.buttonAlign=t.dataset.buttonAlign),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showControls!==void 0&&(e.showControls=t.dataset.showControls==="true"),t.dataset.showInfo!==void 0&&(e.showInfo=t.dataset.showInfo==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.album&&(e.album=t.dataset.album),t.dataset.artwork&&(e.artwork=t.dataset.artwork),t.dataset.waveform&&(e.waveform=t.dataset.waveform),t.dataset.markers)try{e.markers=JSON.parse(t.dataset.markers)}catch(i){console.warn("Invalid markers JSON:",i)}if(t.dataset.playbackRate&&(e.playbackRate=parseFloat(t.dataset.playbackRate)),t.dataset.showPlaybackSpeed!==void 0&&(e.showPlaybackSpeed=t.dataset.showPlaybackSpeed==="true"),t.dataset.playbackRates)try{e.playbackRates=JSON.parse(t.dataset.playbackRates)}catch(i){console.warn("Invalid playbackRates JSON:",i)}return t.dataset.enableMediaSession!==void 0&&(e.enableMediaSession=t.dataset.enableMediaSession==="true"),e}function C(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),i=Math.floor(t%60);return`${e}:${i.toString().padStart(2,"0")}`}function R(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function B(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function P(...t){let e={};for(let i of t)for(let o in i)i[o]!==null&&i[o]!==void 0&&(e[o]=i[o]);return e}function I(t,e){let i;return function(...s){let n=()=>{clearTimeout(i),t(...s)};clearTimeout(i),i=setTimeout(n,e)}}function S(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let i=[];if(e>t.length){let o=(t.length-1)/(e-1);for(let s=0;s<e;s++){let n=s*o,r=Math.floor(n),a=Math.ceil(n),h=n-r;if(a>=t.length)i.push(t[t.length-1]);else if(r===a)i.push(t[r]);else{let l=t[r]*(1-h)+t[a]*h;i.push(l)}}}else{let o=t.length/e;for(let s=0;s<e;s++){let n=Math.floor(s*o),r=Math.floor((s+1)*o),a=0,h=0;for(let l=n;l<=r&&l<t.length;l++)t[l]>a&&(a=t[l]),h++;if(h===0){let l=Math.min(Math.round(s*o),t.length-1);a=t[l]}i.push(a)}}return i}function M(t,e,i,o,s){let n=window.devicePixelRatio||1,r=s.barWidth*n,a=s.barSpacing*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=o*e.width;t.clearRect(0,0,e.width,e.height);for(let y=0;y<l.length;y++){let f=y*(r+a);if(f+r>e.width)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.color,t.fillRect(f,m,r,c)}t.save(),t.beginPath(),t.rect(0,0,p,d),t.clip();for(let y=0;y<l.length;y++){let f=y*(r+a);if(f>p)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.progressColor,t.fillRect(f,m,r,c)}t.restore()}function U(t,e,i,o,s){let n=window.devicePixelRatio||1,r=s.barWidth*n,a=s.barSpacing*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=d/2,y=o*e.width;t.clearRect(0,0,e.width,e.height);for(let f=0;f<l.length;f++){let c=f*(r+a);if(c+r>e.width)break;let m=l[f]*d*.45;t.fillStyle=s.color,t.fillRect(c,p-m,r,m),t.fillRect(c,p,r,m)}t.save(),t.beginPath(),t.rect(0,0,y,d),t.clip();for(let f=0;f<l.length;f++){let c=f*(r+a);if(c>y)break;let m=l[f]*d*.45;t.fillStyle=s.progressColor,t.fillRect(c,p-m,r,m),t.fillRect(c,p,r,m)}t.restore()}function F(t,e,i,o,s){let n=e.width,r=e.height,a=r/2,h=r*.35;t.clearRect(0,0,n,r);let l=(d,p,y=1,f=!1)=>{f&&(t.shadowBlur=12,t.shadowColor=d),t.strokeStyle=d,t.lineWidth=p,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,a);let c=[],m=Math.floor(i.length*y);for(let u=0;u<m;u++){let v=u/(i.length-1)*n,k=i[u],b=Math.sin(u*.1)*k,w=a+b*h;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,k=c[u].y,b=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,k,b,w,c[u+1].x,c[u+1].y)}t.stroke(),f&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,a),t.lineTo(n,a),t.stroke();for(let d=0;d<=10;d++){let p=n/10*d;t.beginPath(),t.moveTo(p,0),t.lineTo(p,r),t.stroke()}l(s.color,2,1,!1),o>0&&l(s.progressColor,3,o,!0)}function W(t,e,i,o,s){let n=window.devicePixelRatio||1,r=(s.barWidth||3)*n,a=(s.barSpacing||1)*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=4*n,y=2*n,f=o*e.width,c=d/2;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let u=m*(r+a);if(u+r>e.width)break;let v=l[m]*d*.9,k=Math.floor(v/(p+y));t.fillStyle=u<f?s.progressColor:s.color;for(let b=0;b<k;b++){let w=b*(p+y);t.fillRect(u,c-w-p,r,p),b>0&&t.fillRect(u,c+w,r,p)}}}function x(t,e,i,o,s){let n=window.devicePixelRatio||1,r=(s.barWidth||2)*n,a=(s.barSpacing||3)*n,h=Math.floor(e.width/(r+a)),l=S(i,h),d=e.height,p=Math.max(1.5*n,r/2),y=o*e.width,f=d/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let m=c*(r+a)+r/2;if(m>e.width)break;let u=l[c]*d*.9;t.fillStyle=m<y?s.progressColor:s.color,t.beginPath(),t.arc(m,f-u/2,p,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(m,f+u/2,p,0,Math.PI*2),t.fill()}}function N(t,e,i,o,s){let n=e.width,r=e.height,a=r/2,h=4,l=h/2;if(t.clearRect(0,0,n,r),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,a-h/2),t.lineTo(n-l,a-h/2),t.arc(n-l,a,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,a+h/2),t.arc(l,a,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),o>0){let d=Math.max(l*2,o*n);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,a-h/2),t.lineTo(d-l,a-h/2),t.arc(d-l,a,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,a+h/2),t.arc(l,a,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let p=8,y=d;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(y,a,p,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(y,a,p*.4,0,Math.PI*2),t.fill()}}var Y={bars:M,bar:M,mirror:U,line:F,blocks:W,block:W,dots:x,dot:x,seekbar:N};function D(t,e,i,o,s){(Y[s.waveformStyle]||M)(t,e,i,o,s)}function z(t){try{let e=t.getChannelData(0),i=t.sampleRate,o=V(e,i);if(o.length<2)return 120;let s=[];for(let h=1;h<o.length;h++)s.push((o[h]-o[h-1])/i);let n={};s.forEach(h=>{let l=60/h,d=Math.round(l/3)*3;d>60&&d<200&&(n[d]=(n[d]||0)+1)});let r=0,a=120;for(let[h,l]of Object.entries(n))l>r&&(r=l,a=parseInt(h));return a<70&&n[a*2]?a*=2:a>160&&n[Math.round(a/2)]&&(a=Math.round(a/2)),a-1}catch(e){return console.warn("BPM detection failed:",e),null}}function V(t,e){let s=[],n=0;for(let r=0;r<t.length-2048;r+=1024){let a=0;for(let d=r;d<r+2048;d++)a+=t[d]*t[d];a=a/2048;let h=a-n,l=n*1.8+.01;if(h>l&&a>.01){let d=s[s.length-1]||0,p=e*.15;r-d>p&&s.push(r)}n=a*.8+n*.2}return s}function j(t,e=200){let i=t.length/e,o=~~(i/10)||1,s=t.numberOfChannels,n=[];for(let a=0;a<s;a++){let h=t.getChannelData(a);for(let l=0;l<e;l++){let d=~~(l*i),p=~~(d+i),y=0,f=0;for(let m=d;m<p;m+=o){let u=h[m];u>f&&(f=u),u<y&&(y=u)}let c=Math.max(Math.abs(f),Math.abs(y));(a===0||c>n[l])&&(n[l]=c)}}let r=Math.max(...n);return r>0?n.map(a=>a/r):n}async function E(t,e=200,i=!1){try{let o=new(window.AudioContext||window.webkitAudioContext),n=await(await fetch(t)).arrayBuffer(),r=await o.decodeAudioData(n),a=j(r,e);a=J(a);let h=null;return i&&(h=await z(r)),o.close(),{peaks:a,bpm:h}}catch(o){throw console.error("Failed to generate waveform:",o),o}}function O(t=200){let e=[];for(let i=0;i<t;i++){let o=Math.random()*.5+.3,s=Math.sin(i/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,o+s)))}return e}function J(t,e=.95){let i=Math.max(...t);if(i===0||i>e)return t;let o=e/i;return t.map(s=>s*o)}function G(){let t=document.documentElement,e=document.body;if(t.classList.contains("dark")||t.classList.contains("dark-mode")||t.classList.contains("theme-dark")||t.getAttribute("data-theme")==="dark"||t.getAttribute("data-color-scheme")==="dark"||e.classList.contains("dark")||e.classList.contains("dark-mode")||e.getAttribute("data-theme")==="dark")return"dark";if(t.classList.contains("light")||t.classList.contains("light-mode")||t.classList.contains("theme-light")||t.getAttribute("data-theme")==="light"||t.getAttribute("data-color-scheme")==="light"||e.classList.contains("light")||e.classList.contains("light-mode")||e.getAttribute("data-theme")==="light")return"light";try{let o=getComputedStyle(document.body).backgroundColor.match(/\d+/g);if(o&&o.length>=3){let[s,n,r]=o.map(Number),a=(s*299+n*587+r*114)/1e3;if(a>128)return"light";if(a<128)return"dark"}}catch{}if(window.matchMedia){if(window.matchMedia("(prefers-color-scheme: dark)").matches)return"dark";if(window.matchMedia("(prefers-color-scheme: light)").matches)return"light"}return"dark"}var T={dark:{waveformColor:"rgba(255, 255, 255, 0.3)",progressColor:"rgba(255, 255, 255, 0.9)",buttonColor:"rgba(255, 255, 255, 0.9)",buttonHoverColor:"rgba(255, 255, 255, 1)",textColor:"#ffffff",textSecondaryColor:"rgba(255, 255, 255, 0.6)",backgroundColor:"rgba(255, 255, 255, 0.03)",borderColor:"rgba(255, 255, 255, 0.1)"},light:{waveformColor:"rgba(0, 0, 0, 0.2)",progressColor:"rgba(0, 0, 0, 0.8)",buttonColor:"rgba(0, 0, 0, 0.8)",buttonHoverColor:"rgba(0, 0, 0, 0.9)",textColor:"#333333",textSecondaryColor:"rgba(0, 0, 0, 0.6)",backgroundColor:"rgba(0, 0, 0, 0.02)",borderColor:"rgba(0, 0, 0, 0.1)"}};function H(t){if(t&&T[t])return T[t];let e=G();return T[e]}var $={url:"",height:60,samples:200,preload:"metadata",playbackRate:1,showPlaybackSpeed:!1,playbackRates:[.5,.75,1,1.25,1.5,1.75,2],buttonAlign:"auto",waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:null,waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showControls:!0,showInfo:!0,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,enableMediaSession:!0,markers:[],showMarkers:!0,title:null,subtitle:null,artwork:null,album:"",playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},q={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var g=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,i={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let o=A(this.container);this.options=P($,o,i);let s=H(this.options.colorPreset);for(let[r,a]of Object.entries(s))(this.options[r]===null||this.options[r]===void 0)&&(this.options[r]=a);let n=q[this.options.waveformStyle];n&&(o.barWidth===void 0&&i.barWidth===void 0&&(this.options.barWidth=n.barWidth),o.barSpacing===void 0&&i.barSpacing===void 0&&(this.options.barSpacing=n.barSpacing)),this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||R(this.options.url),t.instances.set(this.id,this),this.init(),setTimeout(()=>{this.container.dispatchEvent(new CustomEvent("waveformplayer:ready",{bubbles:!0,detail:{player:this,url:this.options.url}}))},100)}init(){this.createDOM(),this.createAudio(),this.initPlaybackSpeed(),this.initKeyboardControls(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player";let e=this.options.buttonAlign;e==="auto"&&(this.options.waveformStyle==="bars"?e="bottom":e="center");let i=this.options.showControls?`
|
|
1
|
+
(()=>{function A(t){let e={};if(t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.preload&&(e.preload=t.dataset.preload),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.buttonAlign&&(e.buttonAlign=t.dataset.buttonAlign),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showControls!==void 0&&(e.showControls=t.dataset.showControls==="true"),t.dataset.showInfo!==void 0&&(e.showInfo=t.dataset.showInfo==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.album&&(e.album=t.dataset.album),t.dataset.artwork&&(e.artwork=t.dataset.artwork),t.dataset.waveform&&(e.waveform=t.dataset.waveform),t.dataset.markers)try{e.markers=JSON.parse(t.dataset.markers)}catch(i){console.warn("Invalid markers JSON:",i)}if(t.dataset.playbackRate&&(e.playbackRate=parseFloat(t.dataset.playbackRate)),t.dataset.showPlaybackSpeed!==void 0&&(e.showPlaybackSpeed=t.dataset.showPlaybackSpeed==="true"),t.dataset.playbackRates)try{e.playbackRates=JSON.parse(t.dataset.playbackRates)}catch(i){console.warn("Invalid playbackRates JSON:",i)}return t.dataset.enableMediaSession!==void 0&&(e.enableMediaSession=t.dataset.enableMediaSession==="true"),e}function S(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),i=Math.floor(t%60);return`${e}:${i.toString().padStart(2,"0")}`}function x(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function B(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function C(...t){let e={};for(let i of t)for(let o in i)i[o]!==null&&i[o]!==void 0&&(e[o]=i[o]);return e}function R(t,e){let i;return function(...s){let a=()=>{clearTimeout(i),t(...s)};clearTimeout(i),i=setTimeout(a,e)}}function P(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let i=[];if(e>t.length){let o=(t.length-1)/(e-1);for(let s=0;s<e;s++){let a=s*o,n=Math.floor(a),r=Math.ceil(a),h=a-n;if(r>=t.length)i.push(t[t.length-1]);else if(n===r)i.push(t[n]);else{let l=t[n]*(1-h)+t[r]*h;i.push(l)}}}else{let o=t.length/e;for(let s=0;s<e;s++){let a=Math.floor(s*o),n=Math.floor((s+1)*o),r=0,h=0;for(let l=a;l<=n&&l<t.length;l++)t[l]>r&&(r=t[l]),h++;if(h===0){let l=Math.min(Math.round(s*o),t.length-1);r=t[l]}i.push(r)}}return i}function E(t,e,i,o,s){let a=window.devicePixelRatio||1,n=s.barWidth*a,r=s.barSpacing*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=o*e.width;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let f=m*(n+r);if(f+n>e.width)break;let c=l[m]*d*.9,y=d-c;t.fillStyle=s.color,t.fillRect(f,y,n,c)}t.save(),t.beginPath(),t.rect(0,0,p,d),t.clip();for(let m=0;m<l.length;m++){let f=m*(n+r);if(f>p)break;let c=l[m]*d*.9,y=d-c;t.fillStyle=s.progressColor,t.fillRect(f,y,n,c)}t.restore()}function U(t,e,i,o,s){let a=window.devicePixelRatio||1,n=s.barWidth*a,r=s.barSpacing*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=d/2,m=o*e.width;t.clearRect(0,0,e.width,e.height);for(let f=0;f<l.length;f++){let c=f*(n+r);if(c+n>e.width)break;let y=l[f]*d*.45;t.fillStyle=s.color,t.fillRect(c,p-y,n,y),t.fillRect(c,p,n,y)}t.save(),t.beginPath(),t.rect(0,0,m,d),t.clip();for(let f=0;f<l.length;f++){let c=f*(n+r);if(c>m)break;let y=l[f]*d*.45;t.fillStyle=s.progressColor,t.fillRect(c,p-y,n,y),t.fillRect(c,p,n,y)}t.restore()}function F(t,e,i,o,s){let a=e.width,n=e.height,r=n/2,h=n*.35;t.clearRect(0,0,a,n);let l=(d,p,m=1,f=!1)=>{f&&(t.shadowBlur=12,t.shadowColor=d),t.strokeStyle=d,t.lineWidth=p,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,r);let c=[],y=Math.floor(i.length*m);for(let u=0;u<y;u++){let v=u/(i.length-1)*a,k=i[u],b=Math.sin(u*.1)*k,w=r+b*h;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,k=c[u].y,b=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,k,b,w,c[u+1].x,c[u+1].y)}t.stroke(),f&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,r),t.lineTo(a,r),t.stroke();for(let d=0;d<=10;d++){let p=a/10*d;t.beginPath(),t.moveTo(p,0),t.lineTo(p,n),t.stroke()}l(s.color,2,1,!1),o>0&&l(s.progressColor,3,o,!0)}function I(t,e,i,o,s){let a=window.devicePixelRatio||1,n=(s.barWidth||3)*a,r=(s.barSpacing||1)*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=4*a,m=2*a,f=o*e.width,c=d/2;t.clearRect(0,0,e.width,e.height);for(let y=0;y<l.length;y++){let u=y*(n+r);if(u+n>e.width)break;let v=l[y]*d*.9,k=Math.floor(v/(p+m));t.fillStyle=u<f?s.progressColor:s.color;for(let b=0;b<k;b++){let w=b*(p+m);t.fillRect(u,c-w-p,n,p),b>0&&t.fillRect(u,c+w,n,p)}}}function D(t,e,i,o,s){let a=window.devicePixelRatio||1,n=(s.barWidth||2)*a,r=(s.barSpacing||3)*a,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=Math.max(1.5*a,n/2),m=o*e.width,f=d/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let y=c*(n+r)+n/2;if(y>e.width)break;let u=l[c]*d*.9;t.fillStyle=y<m?s.progressColor:s.color,t.beginPath(),t.arc(y,f-u/2,p,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(y,f+u/2,p,0,Math.PI*2),t.fill()}}function N(t,e,i,o,s){let a=e.width,n=e.height,r=n/2,h=4,l=h/2;if(t.clearRect(0,0,a,n),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,r-h/2),t.lineTo(a-l,r-h/2),t.arc(a-l,r,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,r+h/2),t.arc(l,r,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),o>0){let d=Math.max(l*2,o*a);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,r-h/2),t.lineTo(d-l,r-h/2),t.arc(d-l,r,h/2,-Math.PI/2,Math.PI/2),t.lineTo(l,r+h/2),t.arc(l,r,h/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let p=8,m=d;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(m,r,p,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(m,r,p*.4,0,Math.PI*2),t.fill()}}var Y={bars:E,bar:E,mirror:U,line:F,blocks:I,block:I,dots:D,dot:D,seekbar:N};function W(t,e,i,o,s){(Y[s.waveformStyle]||E)(t,e,i,o,s)}function z(t){try{let e=t.getChannelData(0),i=t.sampleRate,o=V(e,i);if(o.length<2)return 120;let s=[];for(let h=1;h<o.length;h++)s.push((o[h]-o[h-1])/i);let a={};s.forEach(h=>{let l=60/h,d=Math.round(l/3)*3;d>60&&d<200&&(a[d]=(a[d]||0)+1)});let n=0,r=120;for(let[h,l]of Object.entries(a))l>n&&(n=l,r=parseInt(h));return r<70&&a[r*2]?r*=2:r>160&&a[Math.round(r/2)]&&(r=Math.round(r/2)),r-1}catch(e){return console.warn("BPM detection failed:",e),null}}function V(t,e){let s=[],a=0;for(let n=0;n<t.length-2048;n+=1024){let r=0;for(let d=n;d<n+2048;d++)r+=t[d]*t[d];r=r/2048;let h=r-a,l=a*1.8+.01;if(h>l&&r>.01){let d=s[s.length-1]||0,p=e*.15;n-d>p&&s.push(n)}a=r*.8+a*.2}return s}function j(t,e=200){let i=t.length/e,o=~~(i/10)||1,s=t.numberOfChannels,a=[];for(let r=0;r<s;r++){let h=t.getChannelData(r);for(let l=0;l<e;l++){let d=~~(l*i),p=~~(d+i),m=0,f=0;for(let y=d;y<p;y+=o){let u=h[y];u>f&&(f=u),u<m&&(m=u)}let c=Math.max(Math.abs(f),Math.abs(m));(r===0||c>a[l])&&(a[l]=c)}}let n=Math.max(...a);return n>0?a.map(r=>r/n):a}async function M(t,e=200,i=!1){try{let o=new(window.AudioContext||window.webkitAudioContext),a=await(await fetch(t)).arrayBuffer(),n=await o.decodeAudioData(a),r=j(n,e);r=J(r);let h=null;return i&&(h=await z(n)),o.close(),{peaks:r,bpm:h}}catch(o){throw console.error("Failed to generate waveform:",o),o}}function O(t=200){let e=[];for(let i=0;i<t;i++){let o=Math.random()*.5+.3,s=Math.sin(i/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,o+s)))}return e}function J(t,e=.95){let i=Math.max(...t);if(i===0||i>e)return t;let o=e/i;return t.map(s=>s*o)}function _(){let t=document.documentElement,e=document.body;if(t.classList.contains("dark")||t.classList.contains("dark-mode")||t.classList.contains("theme-dark")||t.getAttribute("data-theme")==="dark"||t.getAttribute("data-color-scheme")==="dark"||e.classList.contains("dark")||e.classList.contains("dark-mode")||e.getAttribute("data-theme")==="dark")return"dark";if(t.classList.contains("light")||t.classList.contains("light-mode")||t.classList.contains("theme-light")||t.getAttribute("data-theme")==="light"||t.getAttribute("data-color-scheme")==="light"||e.classList.contains("light")||e.classList.contains("light-mode")||e.getAttribute("data-theme")==="light")return"light";try{let o=getComputedStyle(document.body).backgroundColor.match(/\d+/g);if(o&&o.length>=3){let[s,a,n]=o.map(Number),r=(s*299+a*587+n*114)/1e3;if(r>128)return"light";if(r<128)return"dark"}}catch{}if(window.matchMedia){if(window.matchMedia("(prefers-color-scheme: dark)").matches)return"dark";if(window.matchMedia("(prefers-color-scheme: light)").matches)return"light"}return"dark"}var T={dark:{waveformColor:"rgba(255, 255, 255, 0.3)",progressColor:"rgba(255, 255, 255, 0.9)",buttonColor:"rgba(255, 255, 255, 0.9)",buttonHoverColor:"rgba(255, 255, 255, 1)",textColor:"#ffffff",textSecondaryColor:"rgba(255, 255, 255, 0.6)",backgroundColor:"rgba(255, 255, 255, 0.03)",borderColor:"rgba(255, 255, 255, 0.1)"},light:{waveformColor:"rgba(0, 0, 0, 0.2)",progressColor:"rgba(0, 0, 0, 0.8)",buttonColor:"rgba(0, 0, 0, 0.8)",buttonHoverColor:"rgba(0, 0, 0, 0.9)",textColor:"#333333",textSecondaryColor:"rgba(0, 0, 0, 0.6)",backgroundColor:"rgba(0, 0, 0, 0.02)",borderColor:"rgba(0, 0, 0, 0.1)"}};function H(t){if(t&&T[t])return T[t];let e=_();return T[e]}var q={url:"",height:60,samples:200,preload:"metadata",audioMode:"self",playbackRate:1,showPlaybackSpeed:!1,playbackRates:[.5,.75,1,1.25,1.5,1.75,2],buttonAlign:"auto",waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:null,waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showControls:!0,showInfo:!0,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,enableMediaSession:!0,markers:[],showMarkers:!0,title:null,subtitle:null,artwork:null,album:"",playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},$={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var g=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,i={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let o=A(this.container);this.options=C(q,o,i);let s=H(this.options.colorPreset);for(let[n,r]of Object.entries(s))(this.options[n]===null||this.options[n]===void 0)&&(this.options[n]=r);let a=$[this.options.waveformStyle];a&&(o.barWidth===void 0&&i.barWidth===void 0&&(this.options.barWidth=a.barWidth),o.barSpacing===void 0&&i.barSpacing===void 0&&(this.options.barSpacing=a.barSpacing)),this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||x(this.options.url),t.instances.set(this.id,this),this.init(),setTimeout(()=>{this.container.dispatchEvent(new CustomEvent("waveformplayer:ready",{bubbles:!0,detail:{player:this,url:this.options.url}}))},100)}init(){this.createDOM(),this.createAudio(),this.initPlaybackSpeed(),this.initKeyboardControls(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player";let e=this.options.buttonAlign;e==="auto"&&(this.options.waveformStyle==="bars"?e="bottom":e="center");let i=this.options.showControls?`
|
|
2
2
|
<button class="waveform-btn" aria-label="Play/Pause" style="
|
|
3
3
|
border-color: ${this.options.buttonColor};
|
|
4
4
|
color: ${this.options.buttonColor};
|
|
@@ -63,4 +63,4 @@
|
|
|
63
63
|
${o}
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
|
-
`,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.artworkEl=this.container.querySelector(".waveform-artwork"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.markersContainer=this.container.querySelector(".waveform-markers"),this.speedBtn=this.container.querySelector(".speed-btn"),this.speedMenu=this.container.querySelector(".speed-menu"),this.resizeCanvas()}createAudio(){this.audio=new Audio,this.audio.preload=this.options.preload||"metadata",this.audio.crossOrigin="anonymous"}initPlaybackSpeed(){this.options.playbackRate&&this.options.playbackRate!==1&&(this.audio.playbackRate=this.options.playbackRate),this.options.showPlaybackSpeed&&this.initSpeedControls()}initSpeedControls(){let e=this.container.querySelector(".speed-btn"),i=this.container.querySelector(".speed-menu");!e||!i||(e.addEventListener("click",o=>{o.stopPropagation(),i.style.display=i.style.display==="none"?"block":"none"}),document.addEventListener("click",()=>{i.style.display="none"}),i.addEventListener("click",o=>{if(o.stopPropagation(),o.target.classList.contains("speed-option")){let s=parseFloat(o.target.dataset.rate);this.setPlaybackRate(s),i.style.display="none"}}),this.updateSpeedUI())}initKeyboardControls(){this.container.setAttribute("tabindex","-1"),this.container.addEventListener("click",()=>{t.getAllInstances().forEach(e=>{e!==this&&e.container.setAttribute("tabindex","-1")}),this.container.setAttribute("tabindex","0"),this.container.focus()}),this.container.addEventListener("keydown",e=>{if(document.activeElement!==this.container)return;let i=e.key,o=this.audio.currentTime;if(i>="0"&&i<="9"){e.preventDefault(),this.seekToPercent(parseInt(i)/10);return}let s={" ":()=>this.togglePlay(),ArrowLeft:()=>this.seekTo(Math.max(0,o-5)),ArrowRight:()=>this.seekTo(Math.min(this.audio.duration,o+5)),ArrowUp:()=>this.setVolume(Math.min(1,this.audio.volume+.1)),ArrowDown:()=>this.setVolume(Math.max(0,this.audio.volume-.1)),m:()=>this.audio.muted=!this.audio.muted,M:()=>this.audio.muted=!this.audio.muted};s[i]&&(e.preventDefault(),s[i]())})}initMediaSession(){!("mediaSession"in navigator)||!this.options.enableMediaSession||(navigator.mediaSession.metadata=new MediaMetadata({title:this.options.title||"Unknown Track",artist:this.options.subtitle||"",album:this.options.album||"",artwork:this.options.artwork?[{src:this.options.artwork,sizes:"512x512",type:"image/jpeg"}]:[]}),navigator.mediaSession.setActionHandler("play",()=>this.play()),navigator.mediaSession.setActionHandler("pause",()=>this.pause()),navigator.mediaSession.setActionHandler("seekbackward",()=>{this.seekTo(Math.max(0,this.audio.currentTime-10))}),navigator.mediaSession.setActionHandler("seekforward",()=>{this.seekTo(Math.min(this.audio.duration,this.audio.currentTime+10))}),navigator.mediaSession.setActionHandler("seekto",e=>{e.seekTime!==null&&this.seekTo(e.seekTime)}))}bindEvents(){this.playBtn&&this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e)),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),this.resizeHandler=I(()=>this.resizeCanvas(),100),window.addEventListener("resize",this.resizeHandler)}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio.src=e,await new Promise((o,s)=>{let n=()=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",r),o()},r=a=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",r),s(a)};this.audio.addEventListener("loadedmetadata",n),this.audio.addEventListener("error",r)});let i=this.options.title||B(e);if(this.titleEl&&(this.titleEl.textContent=i),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let o=await E(e,this.options.samples,this.options.showBPM);this.waveformData=o.peaks,o.bpm&&(this.detectedBPM=o.bpm,this.updateBPMDisplay())}catch(o){console.warn("Using placeholder waveform:",o),this.waveformData=O(this.options.samples)}this.drawWaveform(),this.renderMarkers(),this.initMediaSession(),this.options.onLoad&&this.options.onLoad(this)}catch(i){console.error("Failed to load audio:",i),this.onError(i)}finally{this.setLoading(!1)}}async loadTrack(e,i=null,o=null,s={}){this.isPlaying&&this.pause(),this.audio.src="",this.audio.load(),this.hasError=!1,this.errorEl&&(this.errorEl.style.display="none"),this.canvas&&(this.canvas.style.opacity="1"),this.playBtn&&(this.playBtn.disabled=!1),this.progress=0,this.waveformData=[],this.options=P(this.options,{url:e,title:i||this.options.title,subtitle:o||this.options.subtitle,...s}),s.preload&&(this.audio.preload=s.preload),this.subtitleEl&&(o?(this.subtitleEl.textContent=o,this.subtitleEl.style.display=""):o===""&&(this.subtitleEl.style.display="none")),s.artwork&&this.artworkEl&&(this.artworkEl.src=s.artwork),this.options.markers=s.markers||[],await this.load(e),this.play().catch(()=>{})}setWaveformData(e){if(typeof e=="string"&&e.trim().endsWith(".json")){fetch(e.trim()).then(i=>i.json()).then(i=>{this.waveformData=Array.isArray(i)?i:i.peaks||[],i.markers&&!this.options.markers?.length&&(this.options.markers=i.markers,this.renderMarkers()),this.drawWaveform()}).catch(()=>{});return}if(typeof e=="string")try{let i=JSON.parse(e);this.waveformData=Array.isArray(i)?i:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||D(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){if(!this.canvas||this.isDestroying)return;let e=window.devicePixelRatio||1,i=this.canvas.parentElement.getBoundingClientRect();this.canvas.width=i.width*e,this.canvas.height=this.options.height*e,this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}renderMarkers(){this.markersContainer&&(this.markersContainer.innerHTML="",!(!this.options.showMarkers||!this.options.markers?.length)&&(!this.audio||!this.audio.duration||this.audio.duration===0||this.options.markers.forEach((e,i)=>{if(e.time>this.audio.duration){console.warn(`Marker "${e.label}" at ${e.time}s exceeds audio duration of ${this.audio.duration}s`);return}let o=e.time/this.audio.duration*100,s=document.createElement("button");s.className="waveform-marker",s.style.left=`${o}%`,s.style.backgroundColor=e.color||"rgba(255, 255, 255, 0.5)",s.setAttribute("aria-label",e.label),s.setAttribute("data-time",e.time);let n=document.createElement("span");n.className="waveform-marker-tooltip",n.textContent=e.label,s.appendChild(n),s.addEventListener("click",r=>{r.stopPropagation(),this.seekTo(e.time),this.options.playOnSeek&&!this.isPlaying&&this.play()}),this.markersContainer.appendChild(s)})))}handleCanvasClick(e){if(!this.audio.duration)return;let i=this.canvas.getBoundingClientRect(),o=e.clientX-i.left,s=Math.max(0,Math.min(1,o/i.width));this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.isDestroying||(this.totalTimeEl&&(this.totalTimeEl.textContent=C(this.audio.duration)),this.renderMarkers())}onPlay(){if(!this.isDestroying){if(this.isPlaying=!0,this.playBtn){this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),i&&(i.style.display="flex")}this.startSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)}}onPause(){if(!this.isDestroying){if(this.isPlaying=!1,this.playBtn){this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),i&&(i.style.display="none")}this.stopSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this)}}onEnded(){this.isDestroying||(this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.container.dispatchEvent(new CustomEvent("waveformplayer:ended",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.onPause(),this.options.onEnd&&this.options.onEnd(this))}onError(e){this.isDestroying||(console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this))}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=C(this.audio.currentTime)),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:this.audio.currentTime,duration:this.audio.duration,url:this.options.url}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}updateSpeedUI(){let e=this.container.querySelector(".speed-value");if(e){let i=this.audio.playbackRate;e.textContent=i===1?"1x":`${i}x`}this.container.querySelectorAll(".speed-option").forEach(i=>{i.classList.toggle("active",parseFloat(i.dataset.rate)===this.audio.playbackRate)})}play(){return this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),t.currentlyPlaying=this,this.audio.play()}pause(){t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio.pause()}togglePlay(){this.isPlaying?this.pause():this.play()}seekTo(e){this.audio&&this.audio.duration&&(this.audio.currentTime=Math.max(0,Math.min(e,this.audio.duration)),this.updateProgress())}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}setPlaybackRate(e){if(!this.audio)return;let i=Math.max(.5,Math.min(2,e));this.audio.playbackRate=i,this.options.playbackRate=i,this.updateSpeedUI()}destroy(){this.isDestroying=!0,this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.resizeHandler&&(window.removeEventListener("resize",this.resizeHandler),this.resizeHandler=null),t.instances.delete(this.id),t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio&&(this.audio.pause(),this.audio.src="",this.audio.load(),this.audio=null),this.container.innerHTML="",this.canvas=null,this.ctx=null,this.playBtn=null,this.waveformData=[]}static getInstance(e){if(typeof e=="string"){let i=this.instances.get(e);if(i)return i;let o=document.getElementById(e);if(o)return Array.from(this.instances.values()).find(s=>s.container===o)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(i=>i.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,i=200){try{return(await E(e,i)).peaks}catch(o){throw console.error("Failed to generate waveform:",o),o}}};function L(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new g(e),e.dataset.waveformInitialized="true"}catch(i){console.error("Failed to initialize WaveformPlayer:",i,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L());g.init=L;typeof window<"u"&&(window.WaveformPlayer=g);var lt=g;})();
|
|
66
|
+
`,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.artworkEl=this.container.querySelector(".waveform-artwork"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.markersContainer=this.container.querySelector(".waveform-markers"),this.speedBtn=this.container.querySelector(".speed-btn"),this.speedMenu=this.container.querySelector(".speed-menu"),this.resizeCanvas()}createAudio(){if(this.options.audioMode==="external"){this.audio=null;return}this.audio=new Audio,this.audio.preload=this.options.preload||"metadata",this.audio.crossOrigin="anonymous"}initPlaybackSpeed(){this.audio&&this.options.playbackRate&&this.options.playbackRate!==1&&(this.audio.playbackRate=this.options.playbackRate),this.options.showPlaybackSpeed&&this.initSpeedControls()}initSpeedControls(){let e=this.container.querySelector(".speed-btn"),i=this.container.querySelector(".speed-menu");!e||!i||(e.addEventListener("click",o=>{o.stopPropagation(),i.style.display=i.style.display==="none"?"block":"none"}),document.addEventListener("click",()=>{i.style.display="none"}),i.addEventListener("click",o=>{if(o.stopPropagation(),o.target.classList.contains("speed-option")){let s=parseFloat(o.target.dataset.rate);this.setPlaybackRate(s),i.style.display="none"}}),this.updateSpeedUI())}initKeyboardControls(){this.container.setAttribute("tabindex","-1"),this.container.addEventListener("click",()=>{t.getAllInstances().forEach(e=>{e!==this&&e.container.setAttribute("tabindex","-1")}),this.container.setAttribute("tabindex","0"),this.container.focus()}),this.container.addEventListener("keydown",e=>{if(document.activeElement!==this.container)return;let i=e.key,o=!!this.audio,s=o?this.audio.currentTime:0;if(o&&i>="0"&&i<="9"){e.preventDefault(),this.seekToPercent(parseInt(i)/10);return}let a={" ":()=>this.togglePlay()};o&&(a.ArrowLeft=()=>this.seekTo(Math.max(0,s-5)),a.ArrowRight=()=>this.seekTo(Math.min(this.audio.duration,s+5)),a.ArrowUp=()=>this.setVolume(Math.min(1,this.audio.volume+.1)),a.ArrowDown=()=>this.setVolume(Math.max(0,this.audio.volume-.1)),a.m=a.M=()=>this.audio.muted=!this.audio.muted),a[i]&&(e.preventDefault(),a[i]())})}initMediaSession(){!("mediaSession"in navigator)||!this.options.enableMediaSession||this.audio&&(navigator.mediaSession.metadata=new MediaMetadata({title:this.options.title||"Unknown Track",artist:this.options.subtitle||"",album:this.options.album||"",artwork:this.options.artwork?[{src:this.options.artwork,sizes:"512x512",type:"image/jpeg"}]:[]}),navigator.mediaSession.setActionHandler("play",()=>this.play()),navigator.mediaSession.setActionHandler("pause",()=>this.pause()),navigator.mediaSession.setActionHandler("seekbackward",()=>{this.seekTo(Math.max(0,this.audio.currentTime-10))}),navigator.mediaSession.setActionHandler("seekforward",()=>{this.seekTo(Math.min(this.audio.duration,this.audio.currentTime+10))}),navigator.mediaSession.setActionHandler("seekto",e=>{e.seekTime!==null&&this.seekTo(e.seekTime)}))}bindEvents(){this.playBtn&&this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio&&(this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e))),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),this.resizeHandler=R(()=>this.resizeCanvas(),100),window.addEventListener("resize",this.resizeHandler)}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio&&(this.audio.src=e,await new Promise((o,s)=>{let a=()=>{this.audio.removeEventListener("loadedmetadata",a),this.audio.removeEventListener("error",n),o()},n=r=>{this.audio.removeEventListener("loadedmetadata",a),this.audio.removeEventListener("error",n),s(r)};this.audio.addEventListener("loadedmetadata",a),this.audio.addEventListener("error",n)}));let i=this.options.title||B(e);if(this.titleEl&&(this.titleEl.textContent=i),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let o=await M(e,this.options.samples,this.options.showBPM);this.waveformData=o.peaks,o.bpm&&(this.detectedBPM=o.bpm,this.updateBPMDisplay())}catch(o){console.warn("Using placeholder waveform:",o),this.waveformData=O(this.options.samples)}this.drawWaveform(),this.renderMarkers(),this.initMediaSession(),this.options.onLoad&&this.options.onLoad(this)}catch(i){console.error("Failed to load audio:",i),this.onError(i)}finally{this.setLoading(!1)}}async loadTrack(e,i=null,o=null,s={}){this.isPlaying&&this.pause(),this.audio&&(this.audio.src="",this.audio.load()),this.hasError=!1,this.errorEl&&(this.errorEl.style.display="none"),this.canvas&&(this.canvas.style.opacity="1"),this.playBtn&&(this.playBtn.disabled=!1),this.progress=0,this.waveformData=[],this.options=C(this.options,{url:e,title:i||this.options.title,subtitle:o||this.options.subtitle,...s}),s.preload&&this.audio&&(this.audio.preload=s.preload),this.subtitleEl&&(o?(this.subtitleEl.textContent=o,this.subtitleEl.style.display=""):o===""&&(this.subtitleEl.style.display="none")),s.artwork&&this.artworkEl&&(this.artworkEl.src=s.artwork),this.options.markers=s.markers||[],await this.load(e),this.play().catch(()=>{})}setWaveformData(e){if(typeof e=="string"&&e.trim().endsWith(".json")){fetch(e.trim()).then(i=>i.json()).then(i=>{this.waveformData=Array.isArray(i)?i:i.peaks||[],i.markers&&!this.options.markers?.length&&(this.options.markers=i.markers,this.renderMarkers()),this.drawWaveform()}).catch(()=>{});return}if(typeof e=="string")try{let i=JSON.parse(e);this.waveformData=Array.isArray(i)?i:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||W(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){if(!this.canvas||this.isDestroying)return;let e=window.devicePixelRatio||1,i=this.canvas.parentElement.getBoundingClientRect();this.canvas.width=i.width*e,this.canvas.height=this.options.height*e,this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}renderMarkers(){this.markersContainer&&(this.markersContainer.innerHTML="",!(!this.options.showMarkers||!this.options.markers?.length)&&(!this.audio||!this.audio.duration||this.audio.duration===0||this.options.markers.forEach((e,i)=>{if(e.time>this.audio.duration){console.warn(`Marker "${e.label}" at ${e.time}s exceeds audio duration of ${this.audio.duration}s`);return}let o=e.time/this.audio.duration*100,s=document.createElement("button");s.className="waveform-marker",s.style.left=`${o}%`,s.style.backgroundColor=e.color||"rgba(255, 255, 255, 0.5)",s.setAttribute("aria-label",e.label),s.setAttribute("data-time",e.time);let a=document.createElement("span");a.className="waveform-marker-tooltip",a.textContent=e.label,s.appendChild(a),s.addEventListener("click",n=>{n.stopPropagation(),this.seekTo(e.time),this.options.playOnSeek&&!this.isPlaying&&this.play()}),this.markersContainer.appendChild(s)})))}handleCanvasClick(e){let i=this.canvas.getBoundingClientRect(),o=e.clientX-i.left,s=Math.max(0,Math.min(1,o/i.width));if(this.options.audioMode==="external"){let a=new CustomEvent("waveformplayer:request-seek",{bubbles:!0,cancelable:!0,detail:{...this._buildTrackDetail(),percent:s}});this.container.dispatchEvent(a),a.defaultPrevented||(this.progress=s,this.drawWaveform?.());return}!this.audio||!this.audio.duration||this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.isDestroying||(this.totalTimeEl&&(this.totalTimeEl.textContent=S(this.audio.duration)),this.renderMarkers())}onPlay(){if(!this.isDestroying){if(this.isPlaying=!0,this.playBtn){this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),i&&(i.style.display="flex")}this.startSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)}}onPause(){if(!this.isDestroying){if(this.isPlaying=!1,this.playBtn){this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),i=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),i&&(i.style.display="none")}this.stopSmoothUpdate(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this)}}onEnded(){this.isDestroying||(this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.container.dispatchEvent(new CustomEvent("waveformplayer:ended",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.onPause(),this.options.onEnd&&this.options.onEnd(this))}onError(e){this.isDestroying||(console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this))}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio||!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=S(this.audio.currentTime)),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:this.audio.currentTime,duration:this.audio.duration,url:this.options.url}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}updateSpeedUI(){let e=this.container.querySelector(".speed-value");if(e){let i=this.audio.playbackRate;e.textContent=i===1?"1x":`${i}x`}this.container.querySelectorAll(".speed-option").forEach(i=>{i.classList.toggle("active",parseFloat(i.dataset.rate)===this.audio.playbackRate)})}play(){if(this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),this.options.audioMode==="external"){let e=new CustomEvent("waveformplayer:request-play",{bubbles:!0,cancelable:!0,detail:this._buildTrackDetail()});this.container.dispatchEvent(e),e.defaultPrevented||(t.currentlyPlaying=this);return}return t.currentlyPlaying=this,this.audio.play()}pause(){if(t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.options.audioMode==="external"){this.container.dispatchEvent(new CustomEvent("waveformplayer:request-pause",{bubbles:!0,cancelable:!0,detail:this._buildTrackDetail()}));return}this.audio.pause()}_buildTrackDetail(){return{url:this.options.url,title:this.options.title,subtitle:this.options.subtitle,artist:this.options.artist,artwork:this.options.artwork,id:this.id,player:this}}setPlayingState(e){let i=this.isPlaying;if(this.isPlaying=!!e,this.playBtn){this.playBtn.classList.toggle("playing",this.isPlaying);let o=this.playBtn.querySelector(".waveform-icon-play"),s=this.playBtn.querySelector(".waveform-icon-pause");o&&(o.style.display=this.isPlaying?"none":"flex"),s&&(s.style.display=this.isPlaying?"flex":"none")}this.isPlaying&&!i?(this.startSmoothUpdate?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:play",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPlay&&this.options.onPlay(this)):!this.isPlaying&&i&&(this.stopSmoothUpdate?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:pause",{bubbles:!0,detail:{player:this,url:this.options.url}})),this.options.onPause&&this.options.onPause(this))}setProgress(e,i){!i||i<=0||(this.progress=Math.max(0,Math.min(1,e/i)),this.currentTimeEl&&(this.currentTimeEl.textContent=S(e)),this.totalTimeEl&&(!this.totalTimeEl.dataset._extSet||this._extDuration!==i)&&(this.totalTimeEl.textContent=S(i),this.totalTimeEl.dataset._extSet="1",this._extDuration=i),this.drawWaveform?.(),this.container.dispatchEvent(new CustomEvent("waveformplayer:timeupdate",{bubbles:!0,detail:{player:this,currentTime:e,duration:i,progress:this.progress}})),this.options.onTimeUpdate&&this.options.onTimeUpdate(this,e,i))}togglePlay(){this.isPlaying?this.pause():this.play()}seekTo(e){this.audio&&this.audio.duration&&(this.audio.currentTime=Math.max(0,Math.min(e,this.audio.duration)),this.updateProgress())}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}setPlaybackRate(e){if(!this.audio)return;let i=Math.max(.5,Math.min(2,e));this.audio.playbackRate=i,this.options.playbackRate=i,this.updateSpeedUI()}destroy(){this.isDestroying=!0,this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.resizeHandler&&(window.removeEventListener("resize",this.resizeHandler),this.resizeHandler=null),t.instances.delete(this.id),t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio&&(this.audio.pause(),this.audio.src="",this.audio.load(),this.audio=null),this.container.innerHTML="",this.canvas=null,this.ctx=null,this.playBtn=null,this.waveformData=[]}static getInstance(e){if(typeof e=="string"){let i=this.instances.get(e);if(i)return i;let o=document.getElementById(e);if(o)return Array.from(this.instances.values()).find(s=>s.container===o)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(i=>i.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,i=200){try{return(await M(e,i)).peaks}catch(o){throw console.error("Failed to generate waveform:",o),o}}};function L(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new g(e),e.dataset.waveformInitialized="true"}catch(i){console.error("Failed to initialize WaveformPlayer:",i,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L());g.init=L;typeof window<"u"&&(window.WaveformPlayer=g);var lt=g;})();
|
package/package.json
CHANGED
package/src/js/core.js
CHANGED
|
@@ -253,8 +253,18 @@ export class WaveformPlayer {
|
|
|
253
253
|
/**
|
|
254
254
|
* Create audio element
|
|
255
255
|
* @private
|
|
256
|
+
*
|
|
257
|
+
* No-op in `audioMode: 'external'` — the player has no audio of its
|
|
258
|
+
* own; an external controller (e.g. WaveformBar) owns playback and
|
|
259
|
+
* pushes state in via setPlayingState() / setProgress(). The
|
|
260
|
+
* `this.audio` field stays null in that mode; downstream code must
|
|
261
|
+
* null-check it.
|
|
256
262
|
*/
|
|
257
263
|
createAudio() {
|
|
264
|
+
if (this.options.audioMode === 'external') {
|
|
265
|
+
this.audio = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
258
268
|
this.audio = new Audio();
|
|
259
269
|
this.audio.preload = this.options.preload || 'metadata';
|
|
260
270
|
this.audio.crossOrigin = 'anonymous';
|
|
@@ -269,8 +279,12 @@ export class WaveformPlayer {
|
|
|
269
279
|
* @private
|
|
270
280
|
*/
|
|
271
281
|
initPlaybackSpeed() {
|
|
272
|
-
//
|
|
273
|
-
|
|
282
|
+
// External mode has no <audio> element, so the speed control
|
|
283
|
+
// doesn't apply locally — the external controller (e.g.
|
|
284
|
+
// WaveformBar) owns playback rate. Skip the audio init but
|
|
285
|
+
// still bind the speed control UI in case the controller
|
|
286
|
+
// wants to mirror rate changes via events later.
|
|
287
|
+
if (this.audio && this.options.playbackRate && this.options.playbackRate !== 1) {
|
|
274
288
|
this.audio.playbackRate = this.options.playbackRate;
|
|
275
289
|
}
|
|
276
290
|
|
|
@@ -336,30 +350,37 @@ export class WaveformPlayer {
|
|
|
336
350
|
this.container.focus();
|
|
337
351
|
});
|
|
338
352
|
|
|
339
|
-
// Keyboard events
|
|
353
|
+
// Keyboard events. In external mode `this.audio` is null, so
|
|
354
|
+
// seek/volume/mute keys are no-ops (the external controller
|
|
355
|
+
// owns those). Space (togglePlay) still works because togglePlay
|
|
356
|
+
// routes through the request-play/pause events.
|
|
340
357
|
this.container.addEventListener('keydown', (e) => {
|
|
341
358
|
if (document.activeElement !== this.container) return;
|
|
342
359
|
|
|
343
360
|
const key = e.key;
|
|
344
|
-
const
|
|
361
|
+
const hasAudio = !!this.audio;
|
|
362
|
+
const currentTime = hasAudio ? this.audio.currentTime : 0;
|
|
345
363
|
|
|
346
364
|
// Handle number keys 0-9 for seeking
|
|
347
|
-
if (key >= '0' && key <= '9') {
|
|
365
|
+
if (hasAudio && key >= '0' && key <= '9') {
|
|
348
366
|
e.preventDefault();
|
|
349
367
|
this.seekToPercent(parseInt(key) / 10);
|
|
350
368
|
return;
|
|
351
369
|
}
|
|
352
370
|
|
|
353
|
-
// Handle other keys
|
|
371
|
+
// Handle other keys. Space always works (dispatches
|
|
372
|
+
// request-play in external mode); audio-bound keys only
|
|
373
|
+
// when we own the <audio> element.
|
|
354
374
|
const actions = {
|
|
355
375
|
' ': () => this.togglePlay(),
|
|
356
|
-
'ArrowLeft': () => this.seekTo(Math.max(0, currentTime - 5)),
|
|
357
|
-
'ArrowRight': () => this.seekTo(Math.min(this.audio.duration, currentTime + 5)),
|
|
358
|
-
'ArrowUp': () => this.setVolume(Math.min(1, this.audio.volume + 0.1)),
|
|
359
|
-
'ArrowDown': () => this.setVolume(Math.max(0, this.audio.volume - 0.1)),
|
|
360
|
-
'm': () => this.audio.muted = !this.audio.muted,
|
|
361
|
-
'M': () => this.audio.muted = !this.audio.muted
|
|
362
376
|
};
|
|
377
|
+
if (hasAudio) {
|
|
378
|
+
actions['ArrowLeft'] = () => this.seekTo(Math.max(0, currentTime - 5));
|
|
379
|
+
actions['ArrowRight'] = () => this.seekTo(Math.min(this.audio.duration, currentTime + 5));
|
|
380
|
+
actions['ArrowUp'] = () => this.setVolume(Math.min(1, this.audio.volume + 0.1));
|
|
381
|
+
actions['ArrowDown'] = () => this.setVolume(Math.max(0, this.audio.volume - 0.1));
|
|
382
|
+
actions['m'] = actions['M'] = () => this.audio.muted = !this.audio.muted;
|
|
383
|
+
}
|
|
363
384
|
|
|
364
385
|
if (actions[key]) {
|
|
365
386
|
e.preventDefault();
|
|
@@ -374,6 +395,10 @@ export class WaveformPlayer {
|
|
|
374
395
|
*/
|
|
375
396
|
initMediaSession() {
|
|
376
397
|
if (!('mediaSession' in navigator) || !this.options.enableMediaSession) return;
|
|
398
|
+
// Skip Media Session in external mode — the controller (e.g.
|
|
399
|
+
// WaveformBar) owns audio playback and registers its own Media
|
|
400
|
+
// Session handlers; ours would conflict with its.
|
|
401
|
+
if (!this.audio) return;
|
|
377
402
|
|
|
378
403
|
// Set metadata
|
|
379
404
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
@@ -410,21 +435,30 @@ export class WaveformPlayer {
|
|
|
410
435
|
* @private
|
|
411
436
|
*/
|
|
412
437
|
bindEvents() {
|
|
413
|
-
// Play button (only if controls are shown)
|
|
438
|
+
// Play button (only if controls are shown). In external mode
|
|
439
|
+
// togglePlay() dispatches the request-play/pause events so the
|
|
440
|
+
// controller can decide what to do; the click still goes through
|
|
441
|
+
// here.
|
|
414
442
|
if (this.playBtn) {
|
|
415
443
|
this.playBtn.addEventListener('click', () => this.togglePlay());
|
|
416
444
|
}
|
|
417
445
|
|
|
418
|
-
// Audio events
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.audio
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
446
|
+
// Audio events — only when we own an <audio> element. External
|
|
447
|
+
// mode receives state via setPlayingState() / setProgress() from
|
|
448
|
+
// the controller, so we have nothing to listen to here.
|
|
449
|
+
if (this.audio) {
|
|
450
|
+
this.audio.addEventListener('loadstart', () => this.setLoading(true));
|
|
451
|
+
this.audio.addEventListener('loadedmetadata', () => this.onMetadataLoaded());
|
|
452
|
+
this.audio.addEventListener('canplay', () => this.setLoading(false));
|
|
453
|
+
this.audio.addEventListener('play', () => this.onPlay());
|
|
454
|
+
this.audio.addEventListener('pause', () => this.onPause());
|
|
455
|
+
this.audio.addEventListener('ended', () => this.onEnded());
|
|
456
|
+
this.audio.addEventListener('error', (e) => this.onError(e));
|
|
457
|
+
}
|
|
426
458
|
|
|
427
|
-
// Canvas interactions
|
|
459
|
+
// Canvas interactions — seek-on-click. In external mode the
|
|
460
|
+
// canvas click dispatches a `waveformplayer:request-seek` event
|
|
461
|
+
// so the controller can position its own audio element.
|
|
428
462
|
this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));
|
|
429
463
|
|
|
430
464
|
// Window resize - store handler for cleanup
|
|
@@ -463,24 +497,31 @@ export class WaveformPlayer {
|
|
|
463
497
|
this.progress = 0;
|
|
464
498
|
this.hasError = false;
|
|
465
499
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
500
|
+
// In external mode we don't own an <audio> element — skip
|
|
501
|
+
// src assignment + metadata-wait, but still generate the
|
|
502
|
+
// waveform peaks so the canvas can render the visualization.
|
|
503
|
+
// Duration / current time come from the external controller
|
|
504
|
+
// via setProgress().
|
|
505
|
+
if (this.audio) {
|
|
506
|
+
// Set audio source
|
|
507
|
+
this.audio.src = url;
|
|
508
|
+
|
|
509
|
+
// Wait for metadata to load
|
|
510
|
+
await new Promise((resolve, reject) => {
|
|
511
|
+
const metadataHandler = () => {
|
|
512
|
+
this.audio.removeEventListener('loadedmetadata', metadataHandler);
|
|
513
|
+
this.audio.removeEventListener('error', errorHandler);
|
|
514
|
+
resolve();
|
|
515
|
+
};
|
|
516
|
+
const errorHandler = (e) => {
|
|
517
|
+
this.audio.removeEventListener('loadedmetadata', metadataHandler);
|
|
518
|
+
this.audio.removeEventListener('error', errorHandler);
|
|
519
|
+
reject(e);
|
|
520
|
+
};
|
|
521
|
+
this.audio.addEventListener('loadedmetadata', metadataHandler);
|
|
522
|
+
this.audio.addEventListener('error', errorHandler);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
484
525
|
|
|
485
526
|
// Set title
|
|
486
527
|
const title = this.options.title || extractTitleFromUrl(url);
|
|
@@ -538,9 +579,11 @@ export class WaveformPlayer {
|
|
|
538
579
|
this.pause();
|
|
539
580
|
}
|
|
540
581
|
|
|
541
|
-
// Reset audio element completely
|
|
542
|
-
this.audio
|
|
543
|
-
|
|
582
|
+
// Reset audio element completely (only when we own one)
|
|
583
|
+
if (this.audio) {
|
|
584
|
+
this.audio.src = '';
|
|
585
|
+
this.audio.load();
|
|
586
|
+
}
|
|
544
587
|
|
|
545
588
|
// Clear any errors
|
|
546
589
|
this.hasError = false;
|
|
@@ -567,7 +610,7 @@ export class WaveformPlayer {
|
|
|
567
610
|
});
|
|
568
611
|
|
|
569
612
|
// Apply preload setting if it was changed
|
|
570
|
-
if (options.preload) {
|
|
613
|
+
if (options.preload && this.audio) {
|
|
571
614
|
this.audio.preload = options.preload;
|
|
572
615
|
}
|
|
573
616
|
|
|
@@ -731,12 +774,31 @@ export class WaveformPlayer {
|
|
|
731
774
|
* @private
|
|
732
775
|
*/
|
|
733
776
|
handleCanvasClick(event) {
|
|
734
|
-
|
|
735
|
-
|
|
777
|
+
// In external mode the player has no audio of its own —
|
|
778
|
+
// dispatch a cancelable `waveformplayer:request-seek` event
|
|
779
|
+
// with the target percentage so the controller can seek its
|
|
780
|
+
// own audio. Locally we just update the visual progress so
|
|
781
|
+
// the canvas paints the new position immediately (the
|
|
782
|
+
// controller's progress event will reconcile shortly after).
|
|
736
783
|
const rect = this.canvas.getBoundingClientRect();
|
|
737
784
|
const x = event.clientX - rect.left;
|
|
738
785
|
const targetPercent = Math.max(0, Math.min(1, x / rect.width));
|
|
739
786
|
|
|
787
|
+
if (this.options.audioMode === 'external') {
|
|
788
|
+
const evt = new CustomEvent('waveformplayer:request-seek', {
|
|
789
|
+
bubbles: true,
|
|
790
|
+
cancelable: true,
|
|
791
|
+
detail: { ...this._buildTrackDetail(), percent: targetPercent }
|
|
792
|
+
});
|
|
793
|
+
this.container.dispatchEvent(evt);
|
|
794
|
+
if (!evt.defaultPrevented) {
|
|
795
|
+
this.progress = targetPercent;
|
|
796
|
+
this.drawWaveform?.();
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (!this.audio || !this.audio.duration) return;
|
|
740
802
|
this.seekToPercent(targetPercent);
|
|
741
803
|
}
|
|
742
804
|
|
|
@@ -901,7 +963,10 @@ export class WaveformPlayer {
|
|
|
901
963
|
this.stopSmoothUpdate();
|
|
902
964
|
|
|
903
965
|
const update = () => {
|
|
904
|
-
|
|
966
|
+
// In external mode the canvas redraws are driven by
|
|
967
|
+
// setProgress() pushes from the controller — no internal
|
|
968
|
+
// RAF needed. Self-mode keeps the smooth-update loop.
|
|
969
|
+
if (this.isPlaying && this.audio && this.audio.duration) {
|
|
905
970
|
this.updateProgress();
|
|
906
971
|
this.updateTimer = requestAnimationFrame(update);
|
|
907
972
|
}
|
|
@@ -926,7 +991,9 @@ export class WaveformPlayer {
|
|
|
926
991
|
* @private
|
|
927
992
|
*/
|
|
928
993
|
updateProgress() {
|
|
929
|
-
|
|
994
|
+
// Self-mode only — external mode receives progress via
|
|
995
|
+
// setProgress() from the controller and never calls this.
|
|
996
|
+
if (!this.audio || !this.audio.duration) return;
|
|
930
997
|
|
|
931
998
|
const newProgress = this.audio.currentTime / this.audio.duration;
|
|
932
999
|
|
|
@@ -992,8 +1059,20 @@ export class WaveformPlayer {
|
|
|
992
1059
|
// ============================================
|
|
993
1060
|
|
|
994
1061
|
/**
|
|
995
|
-
* Play audio
|
|
996
|
-
*
|
|
1062
|
+
* Play audio.
|
|
1063
|
+
*
|
|
1064
|
+
* In `audioMode: 'self'` (default): calls the underlying <audio>
|
|
1065
|
+
* element's play(). Returns the promise from HTMLMediaElement.play().
|
|
1066
|
+
*
|
|
1067
|
+
* In `audioMode: 'external'`: dispatches a cancelable
|
|
1068
|
+
* `waveformplayer:request-play` event with the track metadata and
|
|
1069
|
+
* does NOT touch any audio element. Returns `undefined`. An external
|
|
1070
|
+
* controller (e.g. WaveformBar) listens for this event and starts
|
|
1071
|
+
* playback on its own audio source, then pushes state back via
|
|
1072
|
+
* setPlayingState() / setProgress(). Calling preventDefault() on
|
|
1073
|
+
* the event lets the controller veto the play (state is unchanged).
|
|
1074
|
+
*
|
|
1075
|
+
* @return {Promise|undefined}
|
|
997
1076
|
*/
|
|
998
1077
|
play() {
|
|
999
1078
|
if (this.options.singlePlay && WaveformPlayer.currentlyPlaying &&
|
|
@@ -1001,20 +1080,128 @@ export class WaveformPlayer {
|
|
|
1001
1080
|
WaveformPlayer.currentlyPlaying.pause();
|
|
1002
1081
|
}
|
|
1003
1082
|
|
|
1083
|
+
if (this.options.audioMode === 'external') {
|
|
1084
|
+
const evt = new CustomEvent('waveformplayer:request-play', {
|
|
1085
|
+
bubbles: true,
|
|
1086
|
+
cancelable: true,
|
|
1087
|
+
detail: this._buildTrackDetail()
|
|
1088
|
+
});
|
|
1089
|
+
this.container.dispatchEvent(evt);
|
|
1090
|
+
// If the controller cancels (preventDefault), don't claim
|
|
1091
|
+
// "currentlyPlaying" — the controller didn't accept the play.
|
|
1092
|
+
if (!evt.defaultPrevented) {
|
|
1093
|
+
WaveformPlayer.currentlyPlaying = this;
|
|
1094
|
+
}
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1004
1098
|
WaveformPlayer.currentlyPlaying = this;
|
|
1005
1099
|
return this.audio.play();
|
|
1006
1100
|
}
|
|
1007
1101
|
|
|
1008
1102
|
/**
|
|
1009
|
-
* Pause audio
|
|
1103
|
+
* Pause audio.
|
|
1104
|
+
*
|
|
1105
|
+
* In `audioMode: 'external'`, dispatches `waveformplayer:request-pause`
|
|
1106
|
+
* (cancelable) and does NOT touch any audio element. See play().
|
|
1010
1107
|
*/
|
|
1011
1108
|
pause() {
|
|
1012
1109
|
if (WaveformPlayer.currentlyPlaying === this) {
|
|
1013
1110
|
WaveformPlayer.currentlyPlaying = null;
|
|
1014
1111
|
}
|
|
1112
|
+
if (this.options.audioMode === 'external') {
|
|
1113
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:request-pause', {
|
|
1114
|
+
bubbles: true,
|
|
1115
|
+
cancelable: true,
|
|
1116
|
+
detail: this._buildTrackDetail()
|
|
1117
|
+
}));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1015
1120
|
this.audio.pause();
|
|
1016
1121
|
}
|
|
1017
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Build the track detail object dispatched by request-play /
|
|
1125
|
+
* request-pause events in external audio mode. Mirrors the shape
|
|
1126
|
+
* WaveformBar.play() accepts so a controller can forward it
|
|
1127
|
+
* directly: `WaveformBar.play(event.detail)`.
|
|
1128
|
+
*
|
|
1129
|
+
* @private
|
|
1130
|
+
* @return {{url:string,title:?string,subtitle:?string,artist:?string,artwork:?string,player:WaveformPlayer}}
|
|
1131
|
+
*/
|
|
1132
|
+
_buildTrackDetail() {
|
|
1133
|
+
return {
|
|
1134
|
+
url: this.options.url,
|
|
1135
|
+
title: this.options.title,
|
|
1136
|
+
subtitle: this.options.subtitle,
|
|
1137
|
+
artist: this.options.artist,
|
|
1138
|
+
artwork: this.options.artwork,
|
|
1139
|
+
id: this.id,
|
|
1140
|
+
player: this
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* External-mode state pump: flip the play/pause visual state without
|
|
1146
|
+
* touching audio. Mirrors what onPlay()/onPause() do but skips the
|
|
1147
|
+
* audio-element interactions. Safe to call repeatedly — idempotent.
|
|
1148
|
+
*
|
|
1149
|
+
* @param {boolean} playing
|
|
1150
|
+
*/
|
|
1151
|
+
setPlayingState(playing) {
|
|
1152
|
+
const wasPlaying = this.isPlaying;
|
|
1153
|
+
this.isPlaying = !!playing;
|
|
1154
|
+
if (this.playBtn) {
|
|
1155
|
+
this.playBtn.classList.toggle('playing', this.isPlaying);
|
|
1156
|
+
const playIcon = this.playBtn.querySelector('.waveform-icon-play');
|
|
1157
|
+
const pauseIcon = this.playBtn.querySelector('.waveform-icon-pause');
|
|
1158
|
+
if (playIcon) playIcon.style.display = this.isPlaying ? 'none' : 'flex';
|
|
1159
|
+
if (pauseIcon) pauseIcon.style.display = this.isPlaying ? 'flex' : 'none';
|
|
1160
|
+
}
|
|
1161
|
+
if (this.isPlaying && !wasPlaying) {
|
|
1162
|
+
this.startSmoothUpdate?.();
|
|
1163
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:play', {
|
|
1164
|
+
bubbles: true,
|
|
1165
|
+
detail: {player: this, url: this.options.url}
|
|
1166
|
+
}));
|
|
1167
|
+
if (this.options.onPlay) this.options.onPlay(this);
|
|
1168
|
+
} else if (!this.isPlaying && wasPlaying) {
|
|
1169
|
+
this.stopSmoothUpdate?.();
|
|
1170
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:pause', {
|
|
1171
|
+
bubbles: true,
|
|
1172
|
+
detail: {player: this, url: this.options.url}
|
|
1173
|
+
}));
|
|
1174
|
+
if (this.options.onPause) this.options.onPause(this);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* External-mode state pump: update the visualization's progress
|
|
1180
|
+
* from an external clock (e.g. WaveformBar's audio element's
|
|
1181
|
+
* timeupdate). Drives the canvas redraw + the time displays.
|
|
1182
|
+
*
|
|
1183
|
+
* @param {number} currentTime Current playback position in seconds.
|
|
1184
|
+
* @param {number} duration Total track duration in seconds.
|
|
1185
|
+
*/
|
|
1186
|
+
setProgress(currentTime, duration) {
|
|
1187
|
+
if (!duration || duration <= 0) return;
|
|
1188
|
+
this.progress = Math.max(0, Math.min(1, currentTime / duration));
|
|
1189
|
+
// Mirror the existing display update code so callers don't have
|
|
1190
|
+
// to know which DOM elements live where.
|
|
1191
|
+
if (this.currentTimeEl) this.currentTimeEl.textContent = formatTime(currentTime);
|
|
1192
|
+
if (this.totalTimeEl && (!this.totalTimeEl.dataset._extSet || this._extDuration !== duration)) {
|
|
1193
|
+
this.totalTimeEl.textContent = formatTime(duration);
|
|
1194
|
+
this.totalTimeEl.dataset._extSet = '1';
|
|
1195
|
+
this._extDuration = duration;
|
|
1196
|
+
}
|
|
1197
|
+
this.drawWaveform?.();
|
|
1198
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:timeupdate', {
|
|
1199
|
+
bubbles: true,
|
|
1200
|
+
detail: {player: this, currentTime, duration, progress: this.progress}
|
|
1201
|
+
}));
|
|
1202
|
+
if (this.options.onTimeUpdate) this.options.onTimeUpdate(this, currentTime, duration);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1018
1205
|
/**
|
|
1019
1206
|
* Toggle play/pause
|
|
1020
1207
|
*/
|
package/src/js/themes.js
CHANGED
|
@@ -125,6 +125,13 @@ export const DEFAULT_OPTIONS = {
|
|
|
125
125
|
samples: 200,
|
|
126
126
|
preload: 'metadata',
|
|
127
127
|
|
|
128
|
+
// Audio mode — 'self' = player owns the <audio> element (default, current
|
|
129
|
+
// behavior). 'external' = player is a visualization-only surface; no audio
|
|
130
|
+
// element is created, play() dispatches `waveformplayer:request-play`
|
|
131
|
+
// instead of calling audio.play(), and setPlayingState/setProgress are
|
|
132
|
+
// expected to be driven by an external controller (e.g. WaveformBar).
|
|
133
|
+
audioMode: 'self',
|
|
134
|
+
|
|
128
135
|
// Playback
|
|
129
136
|
playbackRate: 1,
|
|
130
137
|
showPlaybackSpeed: false,
|