UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

389 lines (386 loc) • 13.2 kB
import { Audio as Audio$1 } from "./unavailable.mjs"; import { validateCallback } from "../validateCallback.mjs"; import { AudioPlayer, createPlayer } from "./player.mjs"; let player; let playbackError; let permission = "undetermined"; let openingRecording = false; let recording; const remoteListeners = /* @__PURE__ */ new Set(); let remoteInstalled = false; async function getRecordingPermissionStatus() { if (typeof window === "undefined" || !navigator.mediaDevices?.getUserMedia) return "denied"; try { const status = await navigator.permissions.query({ name: "microphone" }); permission = status.state === "prompt" ? "undetermined" : status.state; } catch {} return permission; } async function microphone() { if (!navigator.mediaDevices?.getUserMedia) throw new Error("Audio: microphone recording is unavailable"); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); permission = "granted"; return stream; } catch (error) { if (error.name === "NotAllowedError") permission = "denied"; throw error; } } async function requestRecordingPermission() { if (typeof window === "undefined" || !navigator.mediaDevices?.getUserMedia) return "denied"; try { const stream = await microphone(); stream.getTracks().forEach((track) => track.stop()); } catch (error) { if (error.name !== "NotAllowedError") throw error; } return permission; } function playbackStatus() { if (!player) return { state: "idle", positionMs: 0 }; return { state: player.error || playbackError ? "failed" : player.ended ? "ended" : player.readyState < 2 ? "loading" : player.paused ? "paused" : "playing", uri: player.src, positionMs: player.currentTime * 1e3, ...Number.isFinite(player.duration) && { durationMs: player.duration * 1e3 }, ...playbackError && { error: playbackError } }; } function requirePlayer() { if (!player) throw new Error("Audio: the playback operation is not ready"); return player; } function clearPlayer() { clearNowPlaying(); if (player) { player.pause(); player.removeAttribute("src"); player.load(); player = void 0; } playbackError = void 0; } function recordingDuration() { if (!recording) return 0; return recording.duration + (recording.recorder.state === "recording" ? performance.now() - recording.since : 0); } function recordingStatus() { if (!recording) return { state: "idle", durationMs: 0 }; if (recording.error) throw recording.error; return { state: recording.recorder.state === "paused" ? "paused" : recording.recorder.state === "recording" ? "recording" : "idle", durationMs: recordingDuration() }; } function requireRecorder() { if (!recording || recording.stopping || recording.recorder.state === "inactive") throw recording?.error ?? new Error("Audio: the recording operation is not ready"); return recording; } function recorderChange(event) { const session = requireRecorder(); const target = event === "pause" ? "paused" : "recording"; if (session.recorder.state === target) return Promise.resolve(recordingStatus()); return new Promise((resolve, reject) => { const cleanup = () => { session.recorder.removeEventListener(event, done); session.recorder.removeEventListener("error", failed); }; const done = () => { cleanup(); resolve(recordingStatus()); }; const failed = () => { cleanup(); reject(session.error ?? new Error("Audio: recording failed")); }; session.recorder.addEventListener(event, done, { once: true }); session.recorder.addEventListener("error", failed, { once: true }); if (event === "pause") { session.duration += performance.now() - session.since; session.recorder.pause(); } else { session.since = performance.now(); session.recorder.resume(); } }); } function mediaSession() { if (!navigator.mediaSession) throw new Error("Audio: Media Session is unavailable"); return navigator.mediaSession; } function clearNowPlaying() { if (typeof window === "undefined" || !navigator.mediaSession) return; const session = navigator.mediaSession; if (remoteInstalled) for (const action of [ "play", "pause", "seekto" ]) session.setActionHandler(action, null); remoteInstalled = false; session.metadata = null; session.playbackState = "none"; } function syncNowPlaying() { if (!navigator.mediaSession?.metadata || !player) return; navigator.mediaSession.playbackState = player.paused || player.ended ? "paused" : "playing"; } async function remote(event) { if (!player || !navigator.mediaSession?.metadata) return; const audio = player; try { if (event.type === "play") await Audio.resume(); else if (event.type === "pause") await Audio.pause(); else await Audio.seek(event.positionMs); } catch { return; } if (player !== audio) return; syncNowPlaying(); for (const listener of [...remoteListeners]) listener(event); } function installRemote() { if (remoteInstalled) return; const session = mediaSession(); session.setActionHandler("play", () => remote({ type: "play" })); session.setActionHandler("pause", () => remote({ type: "pause" })); session.setActionHandler("seekto", (event) => remote({ type: "seek", positionMs: (event.seekTime ?? 0) * 1e3 })); remoteInstalled = true; } const Audio = Object.freeze({ getRecordingPermissionStatus, requestRecordingPermission, play: async (uri) => { if (typeof window === "undefined") return Audio$1.play(uri); if (openingRecording || recording) throw new Error("Audio.play: stop the current recording first"); const url = new URL(uri, document.baseURI); if (![ "https:", "http:", "blob:", "data:" ].includes(url.protocol)) throw new Error("Audio.play: invalid browser audio URI"); clearPlayer(); const audio = new window.Audio(url.href); player = audio; for (const event of [ "play", "pause", "ended" ]) audio.addEventListener(event, syncNowPlaying); audio.addEventListener("error", () => { if (player === audio) playbackError = audio.error?.message ?? "Audio.play: decoding failed"; }); try { await audio.play(); } catch (error) { if (player === audio) playbackError = String(error); throw error; } if (player !== audio) throw new Error("Audio.play: playback was replaced"); return playbackStatus(); }, getPlaybackStatus: async () => playbackStatus(), pause: async () => { if (typeof window === "undefined") return Audio$1.pause(); const audio = requirePlayer(); if (!audio.paused) await new Promise((resolve) => { audio.addEventListener("pause", () => resolve(), { once: true }); audio.pause(); }); return playbackStatus(); }, resume: async () => { if (typeof window === "undefined") return Audio$1.resume(); const audio = requirePlayer(); if (audio.ended) audio.currentTime = 0; await audio.play(); if (player !== audio) throw new Error("Audio.resume: playback was replaced"); return playbackStatus(); }, seek: async (positionMs) => { if (typeof window === "undefined") return Audio$1.seek(positionMs); if (!Number.isFinite(positionMs) || positionMs < 0) throw new Error("Audio.seek: position must be finite and nonnegative"); const audio = requirePlayer(); if (audio.readyState < 1) throw new Error("Audio.seek: playback is not ready"); const seconds = Math.min(positionMs / 1e3, Number.isFinite(audio.duration) ? audio.duration : Infinity); if (audio.currentTime === seconds) return playbackStatus(); await new Promise((resolve, reject) => { const cleanup = () => { audio.removeEventListener("seeked", done); audio.removeEventListener("error", failed); audio.removeEventListener("emptied", failed); }; const done = () => { cleanup(); player === audio ? resolve() : reject(new Error("Audio.seek: playback was replaced")); }; const failed = () => { cleanup(); reject(new Error("Audio.seek: playback failed or stopped")); }; audio.addEventListener("seeked", done, { once: true }); audio.addEventListener("error", failed, { once: true }); audio.addEventListener("emptied", failed, { once: true }); audio.currentTime = seconds; }); return playbackStatus(); }, stop: async () => { clearPlayer(); }, startRecording: async () => { if (typeof window === "undefined") return Audio$1.startRecording(); if (player?.ended || player?.error || playbackError) clearPlayer(); if (player || recording || openingRecording) throw new Error("Audio.startRecording: stop the current audio operation first"); if (!window.MediaRecorder) throw new Error("Audio.startRecording: MediaRecorder is unavailable"); openingRecording = true; let stream; try { stream = await microphone(); const recorder = new MediaRecorder(stream); const session = { recorder, stream, chunks: [], duration: 0, since: performance.now(), stopping: false, error: void 0 }; recording = session; recorder.addEventListener("dataavailable", (event) => { if (event.data.size) session.chunks.push(event.data); }); recorder.addEventListener("error", (event) => { session.error = event.error ?? new Error("Audio: recording failed"); stream.getTracks().forEach((track) => track.stop()); }); recorder.start(250); return recordingStatus(); } catch (error) { stream?.getTracks().forEach((track) => track.stop()); recording = void 0; throw error; } finally { openingRecording = false; } }, getRecordingStatus: async () => recordingStatus(), pauseRecording: async () => typeof window === "undefined" ? Audio$1.pauseRecording() : recorderChange("pause"), resumeRecording: async () => typeof window === "undefined" ? Audio$1.resumeRecording() : recorderChange("resume"), stopRecording: async () => { if (typeof window === "undefined") return Audio$1.stopRecording(); if (!recording) throw new Error("Audio.stopRecording: the recording operation is not ready"); const session = recording; if (session.stopping) throw new Error("Audio.stopRecording: recording is already stopping"); const durationMs = recordingDuration(); session.stopping = true; try { if (session.recorder.state !== "inactive") await new Promise((resolve, reject) => { const cleanup = () => { session.recorder.removeEventListener("stop", done); session.recorder.removeEventListener("error", failed); }; const done = () => { cleanup(); resolve(); }; const failed = () => { cleanup(); reject(session.error ?? new Error("Audio: recording failed")); }; session.recorder.addEventListener("stop", done, { once: true }); session.recorder.addEventListener("error", failed, { once: true }); session.recorder.stop(); }); if (session.error) throw session.error; const blob = new Blob(session.chunks, { type: session.recorder.mimeType }); if (!blob.size) throw new Error("Audio.stopRecording: recording produced an empty file"); const uri = URL.createObjectURL(blob); const audio = new window.Audio(); try { await new Promise((resolve, reject) => { const cleanup = () => { clearTimeout(deadline); audio.onloadedmetadata = null; audio.onerror = null; }; const deadline = setTimeout(() => { cleanup(); reject(new Error("Audio.stopRecording: recording did not decode")); }, 1e4); audio.onloadedmetadata = () => { cleanup(); resolve(); }; audio.onerror = () => { cleanup(); reject(new Error("Audio.stopRecording: recording produced no playable audio")); }; audio.src = uri; audio.load(); }); return { uri, durationMs, size: blob.size }; } catch (error) { URL.revokeObjectURL(uri); throw error; } finally { audio.removeAttribute("src"); audio.load(); } } finally { session.stream.getTracks().forEach((track) => track.stop()); recording = void 0; } }, watchInterruptions: Audio$1.watchInterruptions, setNowPlayingInfo: async (info) => { if (typeof window === "undefined") return Audio$1.setNowPlayingInfo(info); if (!info.title.trim()) throw new Error("Audio.setNowPlayingInfo: title must not be empty"); const audio = requirePlayer(); if (info.artworkUri) { const artwork = new window.Image(); artwork.src = info.artworkUri; await artwork.decode(); if (player !== audio) throw new Error("Audio.setNowPlayingInfo: playback was replaced"); } mediaSession().metadata = new MediaMetadata({ title: info.title, artist: info.artist, album: info.albumTitle, artwork: info.artworkUri ? [{ src: info.artworkUri }] : [] }); installRemote(); syncNowPlaying(); }, clearNowPlayingInfo: async () => { if (typeof window === "undefined") return Audio$1.clearNowPlayingInfo(); clearNowPlaying(); }, watchRemoteCommands: (onEvent) => { validateCallback(onEvent, "Audio.watchRemoteCommands: onEvent must be a function"); if (typeof window === "undefined" || !navigator.mediaSession) return Audio$1.watchRemoteCommands(onEvent); remoteListeners.add(onEvent); return () => { remoteListeners.delete(onEvent); }; }, AudioPlayer, createPlayer }); export { Audio }; //# sourceMappingURL=index.mjs.map