@arraypress/waveform-player 1.5.2 → 1.7.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 +59 -0
- package/dist/waveform-player.esm.js +4 -4
- package/dist/waveform-player.js +237 -41
- package/dist/waveform-player.min.js +4 -4
- package/package.json +1 -1
- package/src/js/core.js +287 -51
- package/src/js/themes.js +7 -0
|
@@ -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 a in i)i[a]!==null&&i[a]!==void 0&&(e[a]=i[a]);return e}function R(t,e){let i;return function(...s){let o=()=>{clearTimeout(i),t(...s)};clearTimeout(i),i=setTimeout(o,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 a=(t.length-1)/(e-1);for(let s=0;s<e;s++){let o=s*a,n=Math.floor(o),r=Math.ceil(o),h=o-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 a=t.length/e;for(let s=0;s<e;s++){let o=Math.floor(s*a),n=Math.floor((s+1)*a),r=0,h=0;for(let l=o;l<=n&&l<t.length;l++)t[l]>r&&(r=t[l]),h++;if(h===0){let l=Math.min(Math.round(s*a),t.length-1);r=t[l]}i.push(r)}}return i}function E(t,e,i,a,s){let o=window.devicePixelRatio||1,n=s.barWidth*o,r=s.barSpacing*o,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=a*e.width;t.clearRect(0,0,e.width,e.height);for(let y=0;y<l.length;y++){let f=y*(n+r);if(f+n>e.width)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.color,t.fillRect(f,m,n,c)}t.save(),t.beginPath(),t.rect(0,0,p,d),t.clip();for(let y=0;y<l.length;y++){let f=y*(n+r);if(f>p)break;let c=l[y]*d*.9,m=d-c;t.fillStyle=s.progressColor,t.fillRect(f,m,n,c)}t.restore()}function U(t,e,i,a,s){let o=window.devicePixelRatio||1,n=s.barWidth*o,r=s.barSpacing*o,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=d/2,y=a*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 m=l[f]*d*.45;t.fillStyle=s.color,t.fillRect(c,p-m,n,m),t.fillRect(c,p,n,m)}t.save(),t.beginPath(),t.rect(0,0,y,d),t.clip();for(let f=0;f<l.length;f++){let c=f*(n+r);if(c>y)break;let m=l[f]*d*.45;t.fillStyle=s.progressColor,t.fillRect(c,p-m,n,m),t.fillRect(c,p,n,m)}t.restore()}function F(t,e,i,a,s){let o=e.width,n=e.height,r=n/2,h=n*.35;t.clearRect(0,0,o,n);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,r);let c=[],m=Math.floor(i.length*y);for(let u=0;u<m;u++){let v=u/(i.length-1)*o,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(o,r),t.stroke();for(let d=0;d<=10;d++){let p=o/10*d;t.beginPath(),t.moveTo(p,0),t.lineTo(p,n),t.stroke()}l(s.color,2,1,!1),a>0&&l(s.progressColor,3,a,!0)}function I(t,e,i,a,s){let o=window.devicePixelRatio||1,n=(s.barWidth||3)*o,r=(s.barSpacing||1)*o,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=4*o,y=2*o,f=a*e.width,c=d/2;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let u=m*(n+r);if(u+n>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,n,p),b>0&&t.fillRect(u,c+w,n,p)}}}function D(t,e,i,a,s){let o=window.devicePixelRatio||1,n=(s.barWidth||2)*o,r=(s.barSpacing||3)*o,h=Math.floor(e.width/(n+r)),l=P(i,h),d=e.height,p=Math.max(1.5*o,n/2),y=a*e.width,f=d/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let m=c*(n+r)+n/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,a,s){let o=e.width,n=e.height,r=n/2,h=4,l=h/2;if(t.clearRect(0,0,o,n),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,r-h/2),t.lineTo(o-l,r-h/2),t.arc(o-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(),a>0){let d=Math.max(l*2,a*o);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,y=d;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(y,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(y,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,a,s){(Y[s.waveformStyle]||E)(t,e,i,a,s)}function z(t){try{let e=t.getChannelData(0),i=t.sampleRate,a=j(e,i);if(a.length<2)return 120;let s=[];for(let h=1;h<a.length;h++)s.push((a[h]-a[h-1])/i);let o={};s.forEach(h=>{let l=60/h,d=Math.round(l/3)*3;d>60&&d<200&&(o[d]=(o[d]||0)+1)});let n=0,r=120;for(let[h,l]of Object.entries(o))l>n&&(n=l,r=parseInt(h));return r<70&&o[r*2]?r*=2:r>160&&o[Math.round(r/2)]&&(r=Math.round(r/2)),r-1}catch(e){return console.warn("BPM detection failed:",e),null}}function j(t,e){let s=[],o=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-o,l=o*1.8+.01;if(h>l&&r>.01){let d=s[s.length-1]||0,p=e*.15;n-d>p&&s.push(n)}o=r*.8+o*.2}return s}function V(t,e=200){let i=t.length/e,a=~~(i/10)||1,s=t.numberOfChannels,o=[];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),y=0,f=0;for(let m=d;m<p;m+=a){let u=h[m];u>f&&(f=u),u<y&&(y=u)}let c=Math.max(Math.abs(f),Math.abs(y));(r===0||c>o[l])&&(o[l]=c)}}let n=Math.max(...o);return n>0?o.map(r=>r/n):o}async function M(t,e=200,i=!1){try{let a=new(window.AudioContext||window.webkitAudioContext),o=await(await fetch(t)).arrayBuffer(),n=await a.decodeAudioData(o),r=V(n,e);r=J(r);let h=null;return i&&(h=await z(n)),a.close(),{peaks:r,bpm:h}}catch(a){throw console.error("Failed to generate waveform:",a),a}}function O(t=200){let e=[];for(let i=0;i<t;i++){let a=Math.random()*.5+.3,s=Math.sin(i/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,a+s)))}return e}function J(t,e=.95){let i=Math.max(...t);if(i===0||i>e)return t;let a=e/i;return t.map(s=>s*a)}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 a=getComputedStyle(document.body).backgroundColor.match(/\d+/g);if(a&&a.length>=3){let[s,o,n]=a.map(Number),r=(s*299+o*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 a=A(this.container);this.options=C(q,a,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 o=$[this.options.waveformStyle];o&&(a.barWidth===void 0&&i.barWidth===void 0&&(this.options.barWidth=o.barWidth),a.barSpacing===void 0&&i.barSpacing===void 0&&(this.options.barSpacing=o.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};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<span class="waveform-icon-play">${this.options.playIcon}</span>
|
|
7
7
|
<span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
|
|
8
8
|
</button>
|
|
9
|
-
`:"",
|
|
9
|
+
`:"",a=this.options.showInfo?`
|
|
10
10
|
<div class="waveform-info">
|
|
11
11
|
${this.options.artwork?`
|
|
12
12
|
<img class="waveform-artwork" src="${this.options.artwork}" alt="Album artwork" style="
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
|
62
62
|
|
|
63
|
-
${
|
|
63
|
+
${a}
|
|
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",a=>{a.stopPropagation(),i.style.display=i.style.display==="none"?"block":"none"}),document.addEventListener("click",()=>{i.style.display="none"}),i.addEventListener("click",a=>{if(a.stopPropagation(),a.target.classList.contains("speed-option")){let s=parseFloat(a.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,a=!!this.audio,s=a?this.audio.currentTime:0;if(a&&i>="0"&&i<="9"){e.preventDefault(),this.seekToPercent(parseInt(i)/10);return}let o={" ":()=>this.togglePlay()};a&&(o.ArrowLeft=()=>this.seekTo(Math.max(0,s-5)),o.ArrowRight=()=>this.seekTo(Math.min(this.audio.duration,s+5)),o.ArrowUp=()=>this.setVolume(Math.min(1,this.audio.volume+.1)),o.ArrowDown=()=>this.setVolume(Math.max(0,this.audio.volume-.1)),o.m=o.M=()=>this.audio.muted=!this.audio.muted),o[i]&&(e.preventDefault(),o[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((a,s)=>{let o=()=>{this.audio.removeEventListener("loadedmetadata",o),this.audio.removeEventListener("error",n),a()},n=r=>{this.audio.removeEventListener("loadedmetadata",o),this.audio.removeEventListener("error",n),s(r)};this.audio.addEventListener("loadedmetadata",o),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 a=await M(e,this.options.samples,this.options.showBPM);this.waveformData=a.peaks,a.bpm&&(this.detectedBPM=a.bpm,this.updateBPMDisplay())}catch(a){console.warn("Using placeholder waveform:",a),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,a=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:a||this.options.subtitle,...s}),s.preload&&this.audio&&(this.audio.preload=s.preload),this.subtitleEl&&(a?(this.subtitleEl.textContent=a,this.subtitleEl.style.display=""):a===""&&(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 a=e.time/this.audio.duration*100,s=document.createElement("button");s.className="waveform-marker",s.style.left=`${a}%`,s.style.backgroundColor=e.color||"rgba(255, 255, 255, 0.5)",s.setAttribute("aria-label",e.label),s.setAttribute("data-time",e.time);let o=document.createElement("span");o.className="waveform-marker-tooltip",o.textContent=e.label,s.appendChild(o),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(),a=e.clientX-i.left,s=Math.max(0,Math.min(1,a/i.width));if(this.options.audioMode==="external"){let o=new CustomEvent("waveformplayer:request-seek",{bubbles:!0,cancelable:!0,detail:{...this._buildTrackDetail(),percent:s}});this.container.dispatchEvent(o),o.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 a=this.playBtn.querySelector(".waveform-icon-play"),s=this.playBtn.querySelector(".waveform-icon-pause");a&&(a.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 a=document.getElementById(e);if(a)return Array.from(this.instances.values()).find(s=>s.container===a)}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(a){throw console.error("Failed to generate waveform:",a),a}}static getPeaksUrl(e){if(!e)return;let i=e.replace(/\.(mp3|wav|ogg|flac|m4a|aac)(\?[^#]*)?(#.*)?$/i,".json$2$3");return i===e?void 0:i}};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
|
*/
|
|
@@ -1181,4 +1368,53 @@ export class WaveformPlayer {
|
|
|
1181
1368
|
}
|
|
1182
1369
|
}
|
|
1183
1370
|
|
|
1371
|
+
/**
|
|
1372
|
+
* Derive a peaks-JSON URL from an audio URL by swapping the
|
|
1373
|
+
* extension. Strict counterpart to `generateWaveformData()`:
|
|
1374
|
+
* `generateWaveformData` decodes the audio at runtime,
|
|
1375
|
+
* `getPeaksUrl` assumes you generated the peaks at build time
|
|
1376
|
+
* (e.g. with `@arraypress/waveform-gen`) and stored the JSON
|
|
1377
|
+
* alongside the audio file.
|
|
1378
|
+
*
|
|
1379
|
+
* Use the result as the `waveform` option — the player detects
|
|
1380
|
+
* the `.json` suffix, `fetch()`es the file, and skips the Web
|
|
1381
|
+
* Audio decode pass entirely. Big perf win on catalogues with
|
|
1382
|
+
* many tracks (saves ~1-5s decode per file on slow connections).
|
|
1383
|
+
*
|
|
1384
|
+
* Recognised extensions: mp3, wav, ogg, flac, m4a, aac.
|
|
1385
|
+
* Preserves query strings + URL fragments. Returns `undefined`
|
|
1386
|
+
* for unrecognised inputs so callers can pass through
|
|
1387
|
+
* unconditionally:
|
|
1388
|
+
*
|
|
1389
|
+
* new WaveformPlayer('#el', {
|
|
1390
|
+
* url: track.audioUrl,
|
|
1391
|
+
* waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),
|
|
1392
|
+
* });
|
|
1393
|
+
*
|
|
1394
|
+
* @static
|
|
1395
|
+
* @param {string|undefined|null} audioUrl - Audio file URL.
|
|
1396
|
+
* @returns {string|undefined} Peaks JSON URL, or `undefined`
|
|
1397
|
+
* when the input is empty / has no recognised audio extension.
|
|
1398
|
+
*
|
|
1399
|
+
* @example
|
|
1400
|
+
* WaveformPlayer.getPeaksUrl('/audio/track.mp3')
|
|
1401
|
+
* // '/audio/track.json'
|
|
1402
|
+
*
|
|
1403
|
+
* WaveformPlayer.getPeaksUrl('/audio/track.wav?v=2')
|
|
1404
|
+
* // '/audio/track.json?v=2'
|
|
1405
|
+
*
|
|
1406
|
+
* WaveformPlayer.getPeaksUrl(undefined)
|
|
1407
|
+
* // undefined
|
|
1408
|
+
*/
|
|
1409
|
+
static getPeaksUrl(audioUrl) {
|
|
1410
|
+
if (!audioUrl) return undefined;
|
|
1411
|
+
const swapped = audioUrl.replace(
|
|
1412
|
+
/\.(mp3|wav|ogg|flac|m4a|aac)(\?[^#]*)?(#.*)?$/i,
|
|
1413
|
+
'.json$2$3'
|
|
1414
|
+
);
|
|
1415
|
+
/* Nothing changed → unrecognised extension, return undefined
|
|
1416
|
+
* so callers know to fall back to live decoding. */
|
|
1417
|
+
return swapped === audioUrl ? undefined : swapped;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1184
1420
|
}
|