UNPKG

@researchbunny/react-use-audio-player

Version:

React hook for building custom audio playback controls

123 lines (106 loc) 18.1 kB
"use strict";var D=Object.create;var b=Object.defineProperty,B=Object.defineProperties,E=Object.getOwnPropertyDescriptor,_=Object.getOwnPropertyDescriptors,V=Object.getOwnPropertyNames,k=Object.getOwnPropertySymbols,U=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty,W=Object.prototype.propertyIsEnumerable;var M=(a,t,e)=>t in a?b(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e,h=(a,t)=>{for(var e in t||(t={}))x.call(t,e)&&M(a,e,t[e]);if(k)for(var e of k(t))W.call(t,e)&&M(a,e,t[e]);return a},y=(a,t)=>B(a,_(t));var $=(a,t)=>{for(var e in t)b(a,e,{get:t[e],enumerable:!0})},O=(a,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of V(t))!x.call(a,s)&&s!==e&&b(a,s,{get:()=>t[s],enumerable:!(i=E(t,s))||i.enumerable});return a};var I=(a,t,e)=>(e=a!=null?D(U(a)):{},O(t||!a||!a.__esModule?b(e,"default",{value:a,enumerable:!0}):e,a)),j=a=>O(b({},"__esModule",{value:!0}),a);var m=(a,t,e)=>new Promise((i,s)=>{var l=r=>{try{d(e.next(r))}catch(o){s(o)}},n=r=>{try{d(e.throw(r))}catch(o){s(o)}},d=r=>r.done?i(r.value):Promise.resolve(r.value).then(l,n);d((e=e.apply(a,t)).next())});var tt={};$(tt,{AudioPlayerProvider:()=>Z,context:()=>A,useAudioPlayer:()=>C,useAudioPlayerContext:()=>Y});module.exports=j(tt);var w=require("react");var u=I(require("react"),1);var H=require("howler"),S=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 H.Howl(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()}},z=new S,P=z;var p={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 f=class{updateSnapshot(t){this.snapshot=h(h({},this.snapshot),t),this.subscriptions.forEach(e=>e())}updateSnapshotFromHowlState(t){this.updateSnapshot(h({},this.getSnapshotFromHowl(t)))}initHowl(t){var i,s;let e=P.create(t);this.src=t.src,this.howl=e,this.updateSnapshot(y(h({},this.getSnapshotFromHowl(e)),{error:void 0})),e._html5&&((i=e._sounds[0])!=null&&i._node)&&((s=e._sounds[0])==null?void 0:s._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 p;let e=t.state(),i=t.playing(),s=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 s=="object"?!1:s}}constructor(t){this.howl=null,this.src=null,this.subscriptions=new Set,this.snapshot=p,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"),P.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 v="audio-player",G=` "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("${v}", AudioPlayerProcessor); `,K=new Blob([G],{type:"application/javascript"}),q=URL.createObjectURL(K),N=q;var g=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=p,this.reader=null,t!==void 0&&this.initPlayer(t)}updateSnapshot(t){this.snapshot=h(h({},this.snapshot),t),this.subscriptions.forEach(e=>e())}initPlayer(t){return m(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(s=>m(this,null,function*(){let l=!0,n=null;try{for(;s===this.src&&this.reader;){let{done:d,value:r}=yield this.reader.read();if(r){let o;if(l)if(l=!1,r.length>=44){let c=r.buffer.slice(0,44),T=new DataView(c);this.sampleRate=T.getUint32(24,!0),o=r.buffer.slice(44),yield this.initAudioContext(t),o.byteLength%2!==0&&(n=o.slice(o.byteLength-1),o=o.slice(0,o.byteLength-1)),this.duration+=o.byteLength/(2*this.sampleRate),this.sendPcmDataToWorklet(o),this.updateSnapshot({isReady:!0,duration:this.duration}),t.autoplay&&this.play()}else throw new Error("Invalid WAV header: chunk size too small");else{if(o=r.buffer,n){let c=new Uint8Array(1+o.byteLength);c.set(new Uint8Array(n),0),c.set(new Uint8Array(o),n.byteLength),o=c.buffer,n=null}o.byteLength%2!==0&&(n=o.slice(o.byteLength-1),o=o.slice(0,o.byteLength-1)),this.duration+=o.byteLength/(2*this.sampleRate),this.sendPcmDataToWorklet(o),this.updateSnapshot({duration:this.duration})}}if(d){t.onload&&t.onload();break}}}catch(d){this.updateSnapshot({error:`Error processing stream: ${d}`})}}))(t.src)}catch(e){this.updateSnapshot({isLoading:!1,isReady:!1,error:`Failed to load PCM data from URL: ${e}`})}})}initAudioContext(t){return m(this,null,function*(){if(!this.audioContext)try{this.audioContext=new AudioContext({sampleRate:this.sampleRate,latencyHint:"interactive"}),yield this.audioContext.audioWorklet.addModule(N),this.workletNode=new AudioWorkletNode(this.audioContext,v),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:s}=e.data;if(s!==void 0&&(this.currentTime=s,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 m(this,null,function*(){yield this.destroy(),this.initPlayer(t)})}destroy(){return m(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(p)})}subscribe(t){return this.subscriptions.add(t),()=>this.subscriptions.delete(t)}getSnapshot(){return this.snapshot}play(){return m(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 s=Math.max(0,Math.min(1,t)),l=Math.max(0,Math.min(1,e));this.gainNode.gain.setValueAtTime(s,this.audioContext.currentTime),this.gainNode.gain.linearRampToValueAtTime(l,this.audioContext.currentTime+i/1e3),setTimeout(()=>{this.updateSnapshot({volume:l})},i)}};var L=require("ua-parser-js");function J(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 Q(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 R(a){let t=new L.UAParser().getEngine().name==="WebKit";return!!(a!=null&&a.isPCMStream)&&t}function X(a,t,e,i){if(R(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 s=J(e,i);t.current.load(s)}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 s=Q(e,i);a.current.load(s)}}function C(){let[a,t]=u.default.useState(!1),e=(0,u.useRef)(new g),i=(0,u.useRef)(new f),s=(0,u.useSyncExternalStore)(e.current.subscribe.bind(e.current),e.current.getSnapshot.bind(e.current),()=>p),l=(0,u.useSyncExternalStore)(i.current.subscribe.bind(i.current),i.current.getSnapshot.bind(i.current),()=>p);(0,u.useEffect)(()=>()=>{e.current&&e.current.destroy(),i.current&&i.current.destroy()},[]);let n=(0,u.useCallback)((o,c)=>{R(c)&&t(!0),X(i,e,o,c)},[]),d=a?s:l,r=a?e:i;return y(h({},d),{player:r.current instanceof f?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 f,canLoop:()=>r.current instanceof f,canChangeRate:()=>r.current instanceof f})}var F=require("react/jsx-runtime"),A=(0,w.createContext)(null),Y=()=>{let a=(0,w.useContext)(A);if(a===null)throw new Error("useAudioPlayerContext must be used within an AudioPlayerProvider");return a};function Z({children:a}){let t=C();return(0,F.jsx)(A.Provider,{value:t,children:a})}0&&(module.exports={AudioPlayerProvider,context,useAudioPlayer,useAudioPlayerContext}); //# sourceMappingURL=index.cjs.map