UNPKG

@researchbunny/react-use-audio-player

Version:

React hook for building custom audio playback controls

123 lines (106 loc) 17.5 kB
var H=Object.defineProperty,N=Object.defineProperties;var L=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var R=Object.prototype.hasOwnProperty,F=Object.prototype.propertyIsEnumerable;var P=(a,t,e)=>t in a?H(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e,u=(a,t)=>{for(var e in t||(t={}))R.call(t,e)&&P(a,e,t[e]);if(S)for(var e of S(t))F.call(t,e)&&P(a,e,t[e]);return a},m=(a,t)=>N(a,L(t));var f=(a,t,e)=>new Promise((i,o)=>{var l=r=>{try{h(e.next(r))}catch(s){o(s)}},n=r=>{try{h(e.throw(r))}catch(s){o(s)}},h=r=>r.done?i(r.value):Promise.resolve(r.value).then(l,n);h((e=e.apply(a,t)).next())});import{createContext as G,useContext as K}from"react";import V,{useCallback as U,useEffect as W,useRef as C,useSyncExternalStore as A}from"react";import{Howl as T}from"howler";var y=class{constructor(){this._cache=new Map}create(t){let e=t.src;if(this._cache.has(e))return this._cache.get(e);let i=new T(t);return this._cache.set(e,i),i}set(t,e){this._cache.set(t,e)}get(t){return this._cache.get(t)}clear(t){this._cache.delete(t)}destroy(t){let e=this.get(t);e&&(e.unload(),this.clear(t))}reset(){this._cache.values().forEach(t=>t.unload()),this._cache.clear()}},D=new y,g=D;var d={isUnloaded:!0,isLoading:!1,isReady:!1,isLooping:!1,isPlaying:!1,isStopped:!1,isPaused:!1,duration:0,rate:1,volume:1,isMuted:!1,error:void 0};var c=class{updateSnapshot(t){this.snapshot=u(u({},this.snapshot),t),this.subscriptions.forEach(e=>e())}updateSnapshotFromHowlState(t){this.updateSnapshot(u({},this.getSnapshotFromHowl(t)))}initHowl(t){var i,o;let e=g.create(t);this.src=t.src,this.howl=e,this.updateSnapshot(m(u({},this.getSnapshotFromHowl(e)),{error:void 0})),e._html5&&((i=e._sounds[0])!=null&&i._node)&&((o=e._sounds[0])==null?void 0:o._node).addEventListener("canplaythrough",()=>{this.updateSnapshotFromHowlState(e)}),e.on("load",()=>this.updateSnapshotFromHowlState(e)),e.on("play",()=>this.updateSnapshotFromHowlState(e)),e.on("end",()=>this.updateSnapshotFromHowlState(e)),e.on("pause",()=>this.updateSnapshotFromHowlState(e)),e.on("stop",()=>this.updateSnapshotFromHowlState(e)),e.on("mute",()=>this.updateSnapshotFromHowlState(e)),e.on("volume",()=>this.updateSnapshotFromHowlState(e)),e.on("rate",()=>this.updateSnapshotFromHowlState(e)),e.on("seek",()=>this.updateSnapshotFromHowlState(e)),e.on("fade",()=>this.updateSnapshotFromHowlState(e)),e.on("loaderror",(l,n)=>{console.error(`Howl load error: ${n}`),this.updateSnapshotFromHowlState(e),this.updateSnapshot({error:"Failed to load audio source"})}),e.on("playerror",(l,n)=>{console.error(`Howl playback error: ${n}`),this.updateSnapshotFromHowlState(e),this.updateSnapshot({error:"Failed to play audio source"})})}getSnapshotFromHowl(t){if(t.state()==="unloaded")return d;let e=t.state(),i=t.playing(),o=t.mute();return{isUnloaded:e==="unloaded",isLoading:e==="loading",isReady:e==="loaded",isLooping:t.loop(),isPlaying:i,isStopped:!i&&t.seek()===0,isPaused:!i&&t.seek()>0,duration:t.duration(),rate:t.rate(),volume:t.volume(),isMuted:typeof o=="object"?!1:o}}constructor(t){this.howl=null,this.src=null,this.subscriptions=new Set,this.snapshot=d,t!==void 0&&this.initHowl(t)}load(t){this.howl!==null&&this.destroy(),this.initHowl(t)}destroy(){this.src&&this.howl&&(this.howl.off("load"),this.howl.off("play"),this.howl.off("end"),this.howl.off("pause"),this.howl.off("stop"),this.howl.off("mute"),this.howl.off("volume"),this.howl.off("rate"),this.howl.off("seek"),this.howl.off("fade"),this.howl.off("loaderror"),this.howl.off("playerror"),g.destroy(this.src),this.src=null,this.howl=null)}subscribe(t){return this.subscriptions.add(t),()=>this.subscriptions.delete(t)}getSnapshot(){return this.snapshot}play(){if(this.howl){if(this.howl.playing())return;this.howl.play()}}pause(){this.howl&&this.howl.pause()}togglePlayPause(){this.snapshot.isPlaying?this.pause():this.play()}stop(){this.howl&&this.howl.stop()}setVolume(t){this.howl&&this.howl.volume(t)}setRate(t){this.howl&&this.howl.rate(t)}loopOn(){this.howl&&(this.howl.loop(!0),this.updateSnapshotFromHowlState(this.howl))}loopOff(){this.howl&&(this.howl.loop(!1),this.updateSnapshotFromHowlState(this.howl))}toggleLoop(){this.snapshot.isLooping?this.loopOff():this.loopOn()}mute(){this.howl&&this.howl.mute(!0)}unmute(){this.howl&&this.howl.mute(!1)}toggleMute(){this.snapshot.isMuted?this.unmute():this.mute()}seek(t){this.howl&&this.snapshot.duration!==1/0&&this.howl.seek(t)}getPosition(){return this.howl?this.howl.seek():0}fade(t,e,i){this.howl&&this.howl.fade(t,e,i)}};var w="audio-player",B=` "use strict"; /* ---------- AudioPlayerProcessor ---------- */ class AudioPlayerProcessor extends AudioWorkletProcessor { constructor() { super(); this.initBufferStore(); this.state = "idle"; // idle | playing | paused this.currentTime = 0; this.lastEmit = 0; this.totalSamplesEmitted = 0; /* ---- messages from main thread ---- */ this.port.onmessage = (ev) => { const { audioData, play, pause, clear } = ev.data; if (audioData) { this.writeData(audioData); } if (play && this.state !== "playing") { this.state = this.outputBuffers.length ? "playing" : "idle"; this.port.postMessage({ state: this.state }); } if (pause && this.state === "playing") { this.state = "paused"; this.port.postMessage({ state: "paused" }); } if (clear) { this.initBufferStore(); this.currentTime = 0; this.state = "idle"; this.port.postMessage({ state: "idle", currentTime: 0 }); } }; } /** * Initializes the buffer store for audio data. * This sets up the initial buffer length and prepares the output buffers. */ initBufferStore() { this.bufferLength = 128; this.outputBuffers = []; this.writeBuffer = new Float32Array(this.bufferLength); this.writeOffset = 0; } /** * Writes audio data to the output buffers. * @param {ArrayBuffer} audioData - The audio data to write. */ writeData(audioData) { const int16Data = new Int16Array(audioData); const floatData = new Float32Array(int16Data.length); // Convert from Int16 to Float32 (-1.0 to 1.0) for (let i = 0; i < floatData.length; i++) { floatData[i] = int16Data[i] / 0x8000; // Convert Int16 to Float32 } for (let i = 0; i < floatData.length; i++) { this.writeBuffer[this.writeOffset++] = floatData[i]; if (this.writeOffset >= this.bufferLength) { this.writeBuffer = new Float32Array(this.bufferLength); this.outputBuffers.push(this.writeBuffer); this.writeOffset = 0; } } } /** * Read audio data from the output buffers. * @param {number} length - The number of samples to read. * @returns {Float32Array} - The read audio data. */ readData(length) { if (this.outputBuffers.length === 0) { throw new Error("No audio data available to read."); } let output = new Float32Array(length); const buffer = this.outputBuffers.shift(); for (let sampleNum = 0; sampleNum < length; sampleNum++) { output[sampleNum] = buffer[sampleNum] || 0; } return output; } process(_ins, outs) { const chan = outs[0][0]; if (this.state !== "playing") return true; if (this.outputBuffers.length) { const samples = this.readData(chan.length); chan.set(samples); this.currentTime += samples.length / sampleRate; this.totalSamplesEmitted += samples.length; if (this.state === "idle") { this.state = "playing"; this.port.postMessage({ state: "playing", currentTime: this.currentTime }); } else if (this.outputBuffers.length === 0) { let a = 1; // this.state = "idle"; // this.currentTime = 0; // this.port.postMessage({ state: "idle", currentTime: 0 }); } else if (this.currentTime - this.lastEmit > 0.5) { this.lastEmit = this.currentTime; this.port.postMessage({ currentTime: this.currentTime }); } } return true; } } registerProcessor("${w}", AudioPlayerProcessor); `,E=new Blob([B],{type:"application/javascript"}),_=URL.createObjectURL(E),v=_;var b=class{constructor(t){this.reader=null;this.abortController=null;this.src=null,this.audioContext=null,this.workletNode=null,this.gainNode=null,this.sampleRate=24e3,this.position=0,this.duration=0,this.currentTime=0,this.subscriptions=new Set,this.snapshot=d,this.reader=null,t!==void 0&&this.initPlayer(t)}updateSnapshot(t){this.snapshot=u(u({},this.snapshot),t),this.subscriptions.forEach(e=>e())}initPlayer(t){return f(this,null,function*(){this.src=t.src,this.abortController=new AbortController,this.updateSnapshot({isUnloaded:!1,isLoading:!1,isReady:!1,error:void 0});try{let e=yield fetch(t.src,{signal:this.abortController.signal});if(!e.ok)throw new Error(`HTTP error! Status: ${e.status}`);if(!e.body)throw new Error("Response body is not available as a readable stream");this.reader=e.body.getReader(),yield(o=>f(this,null,function*(){let l=!0,n=null;try{for(;o===this.src&&this.reader;){let{done:h,value:r}=yield this.reader.read();if(r){let s;if(l)if(l=!1,r.length>=44){let p=r.buffer.slice(0,44),O=new DataView(p);this.sampleRate=O.getUint32(24,!0),s=r.buffer.slice(44),yield this.initAudioContext(t),s.byteLength%2!==0&&(n=s.slice(s.byteLength-1),s=s.slice(0,s.byteLength-1)),this.duration+=s.byteLength/(2*this.sampleRate),this.sendPcmDataToWorklet(s),this.updateSnapshot({isReady:!0,duration:this.duration}),t.autoplay&&this.play()}else throw new Error("Invalid WAV header: chunk size too small");else{if(s=r.buffer,n){let p=new Uint8Array(1+s.byteLength);p.set(new Uint8Array(n),0),p.set(new Uint8Array(s),n.byteLength),s=p.buffer,n=null}s.byteLength%2!==0&&(n=s.slice(s.byteLength-1),s=s.slice(0,s.byteLength-1)),this.duration+=s.byteLength/(2*this.sampleRate),this.sendPcmDataToWorklet(s),this.updateSnapshot({duration:this.duration})}}if(h){t.onload&&t.onload();break}}}catch(h){this.updateSnapshot({error:`Error processing stream: ${h}`})}}))(t.src)}catch(e){this.updateSnapshot({isLoading:!1,isReady:!1,error:`Failed to load PCM data from URL: ${e}`})}})}initAudioContext(t){return f(this,null,function*(){if(!this.audioContext)try{this.audioContext=new AudioContext({sampleRate:this.sampleRate,latencyHint:"interactive"}),yield this.audioContext.audioWorklet.addModule(v),this.workletNode=new AudioWorkletNode(this.audioContext,w),this.gainNode=this.audioContext.createGain(),this.gainNode.gain.value=this.snapshot.isMuted?0:this.snapshot.volume,this.workletNode.connect(this.gainNode),this.gainNode.connect(this.audioContext.destination),this.workletNode.port.onmessage=e=>{let{state:i,currentTime:o}=e.data;if(o!==void 0&&(this.currentTime=o,this.position=Math.round(this.currentTime)),i)switch(i){case"playing":this.updateSnapshot({isPlaying:!0,isPaused:!1,isStopped:!1}),t.onplay&&t.onplay();break;case"paused":this.updateSnapshot({isPlaying:!1,isPaused:!0,isStopped:!1}),t.onpause&&t.onpause();break;case"idle":this.updateSnapshot({isPlaying:!1,isPaused:!1,isStopped:!0}),t.onstop&&t.onstop();break}}}catch(e){this.updateSnapshot({error:`Failed to initialize audio context: ${e}`})}})}sendPcmDataToWorklet(t){if(this.workletNode){if(t.byteLength%2!==0)throw new Error("PCM data must have an even byte length (16-bit samples)");this.workletNode.port.postMessage({audioData:t},[t])}}load(t){return f(this,null,function*(){yield this.destroy(),this.initPlayer(t)})}destroy(){return f(this,null,function*(){if(this.abortController&&(this.abortController.abort(),this.abortController=null),this.reader){try{yield this.reader.cancel()}catch(t){}this.reader=null}this.snapshot.isPlaying&&this.stop(),this.workletNode&&(this.workletNode.disconnect(),this.workletNode=null),this.gainNode&&(this.gainNode.disconnect(),this.gainNode=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.duration=0,this.currentTime=0,this.src=null,this.sampleRate=24e3,this.updateSnapshot(d)})}subscribe(t){return this.subscriptions.add(t),()=>this.subscriptions.delete(t)}getSnapshot(){return this.snapshot}play(){return f(this,null,function*(){if(!(!this.audioContext||!this.workletNode))try{this.audioContext.state==="suspended"&&(yield this.audioContext.resume()),this.snapshot.isPlaying||this.workletNode.port.postMessage({play:!0})}catch(t){this.updateSnapshot({error:`Failed to play PCM audio: ${t}`})}})}pause(){if(!(!this.workletNode||!this.snapshot.isPlaying))try{this.workletNode.port.postMessage({pause:!0})}catch(t){this.updateSnapshot({error:`Failed to pause PCM audio: ${t}`})}}togglePlayPause(){this.snapshot.isPlaying?this.pause():this.play()}stop(){if(this.workletNode)try{this.workletNode.port.postMessage({clear:!0}),this.currentTime=0,this.position=0}catch(t){this.updateSnapshot({error:`Failed to stop PCM audio: ${t}`})}}setVolume(t){if(this.gainNode){let e=Math.max(0,Math.min(1,t));this.gainNode.gain.value=this.snapshot.isMuted?0:e,this.updateSnapshot({volume:e})}}setRate(t){throw new Error("Setting playback rate is not supported in this implementation")}loopOn(){throw new Error("Looping playback is not supported in this implementation")}loopOff(){throw new Error("Looping playback is not supported in this implementation")}toggleLoop(){this.snapshot.isLooping?this.loopOff():this.loopOn()}mute(){this.gainNode&&(this.gainNode.gain.value=0,this.updateSnapshot({isMuted:!0}))}unmute(){this.gainNode&&(this.gainNode.gain.value=this.snapshot.volume,this.updateSnapshot({isMuted:!1}))}toggleMute(){this.snapshot.isMuted?this.unmute():this.mute()}seek(t){throw Error("Precise seeking is not fully supported in PCM player")}getPosition(){return this.position}fade(t,e,i){if(!this.gainNode||!this.audioContext)return;let o=Math.max(0,Math.min(1,t)),l=Math.max(0,Math.min(1,e));this.gainNode.gain.setValueAtTime(o,this.audioContext.currentTime),this.gainNode.gain.linearRampToValueAtTime(l,this.audioContext.currentTime+i/1e3),setTimeout(()=>{this.updateSnapshot({volume:l})},i)}};import{UAParser as $}from"ua-parser-js";function I(a,t){return{src:a,autoplay:t==null?void 0:t.autoplay,loop:t==null?void 0:t.loop,initialVolume:t==null?void 0:t.initialVolume,initialMute:t==null?void 0:t.initialMute,onload:t==null?void 0:t.onload,onplay:t==null?void 0:t.onplay,onend:t==null?void 0:t.onend,onpause:t==null?void 0:t.onpause,onstop:t==null?void 0:t.onstop}}function j(a,t){return{src:a,format:t==null?void 0:t.format,html5:t==null?void 0:t.html5,autoplay:t==null?void 0:t.autoplay,loop:t==null?void 0:t.loop,initialVolume:t==null?void 0:t.initialVolume,initialMute:t==null?void 0:t.initialMute,initialRate:t==null?void 0:t.initialRate,onload:t==null?void 0:t.onload,onplay:t==null?void 0:t.onplay,onend:t==null?void 0:t.onend,onpause:t==null?void 0:t.onpause,onstop:t==null?void 0:t.onstop}}function k(a){let t=new $().getEngine().name==="WebKit";return!!(a!=null&&a.isPCMStream)&&t}function z(a,t,e,i){if(k(i)){a.current&&(console.log("Destroying Howler instance for PCM stream"),a.current.destroy()),t.current&&(console.log("Destroying PCM instance for Howl stream"),t.current.destroy());let o=I(e,i);t.current.load(o)}else{a.current&&(console.log("Destroying Howler instance for PCM stream"),a.current.destroy()),t.current&&(console.log("Destroying PCM instance for Howl stream"),t.current.destroy());let o=j(e,i);a.current.load(o)}}function M(){let[a,t]=V.useState(!1),e=C(new b),i=C(new c),o=A(e.current.subscribe.bind(e.current),e.current.getSnapshot.bind(e.current),()=>d),l=A(i.current.subscribe.bind(i.current),i.current.getSnapshot.bind(i.current),()=>d);W(()=>()=>{e.current&&e.current.destroy(),i.current&&i.current.destroy()},[]);let n=U((s,p)=>{k(p)&&t(!0),z(i,e,s,p)},[]),h=a?o:l,r=a?e:i;return m(u({},h),{player:r.current instanceof c?r.current.howl:null,src:r.current.src,load:n,play:r.current.play.bind(r.current),pause:r.current.pause.bind(r.current),togglePlayPause:r.current.togglePlayPause.bind(r.current),stop:r.current.stop.bind(r.current),setVolume:r.current.setVolume.bind(r.current),fade:r.current.fade.bind(r.current),mute:r.current.mute.bind(r.current),unmute:r.current.unmute.bind(r.current),toggleMute:r.current.toggleMute.bind(r.current),setRate:r.current.setRate.bind(r.current),seek:r.current.seek.bind(r.current),loopOn:r.current.loopOn.bind(r.current),loopOff:r.current.loopOff.bind(r.current),toggleLoop:r.current.toggleLoop.bind(r.current),getPosition:r.current.getPosition.bind(r.current),cleanup:r.current.destroy.bind(r.current),canSeek:()=>r.current instanceof c,canLoop:()=>r.current instanceof c,canChangeRate:()=>r.current instanceof c})}import{jsx as q}from"react/jsx-runtime";var x=G(null),yt=()=>{let a=K(x);if(a===null)throw new Error("useAudioPlayerContext must be used within an AudioPlayerProvider");return a};function gt({children:a}){let t=M();return q(x.Provider,{value:t,children:a})}export{gt as AudioPlayerProvider,x as context,M as useAudioPlayer,yt as useAudioPlayerContext}; //# sourceMappingURL=index.js.map