one
Version:
One is a React framework for web, iOS, and Android apps.
389 lines (386 loc) • 13.2 kB
JavaScript
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