mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
1,217 lines (1,216 loc) • 36.9 kB
JavaScript
const SAMPLE_RATES = {
/** 语音识别推荐 */
SPEECH: 16e3,
/** 标准音质 */
STANDARD: 22050,
/** CD音质 */
CD: 44100,
/** 高清音质 */
HD: 48e3
};
const CHANNEL_COUNTS = {
/** 单声道,适合语音识别 */
MONO: 1,
/** 立体声,适合音乐录制 */
STEREO: 2
};
const AUDIO_FORMATS = {
/** WAV格式,无损音质,文件较大 */
WAV: "wav",
/** MP3格式,有损压缩,文件小,普遍支持 */
MP3: "mp3",
/** WebM格式,专为网络优化,支持流式传输 */
WEBM: "webm",
/** Ogg格式,开放格式,支持流式传输 */
OGG: "ogg"
};
class AudioProcessingError extends Error {
constructor(message) {
super(message);
this.name = "AudioProcessingError";
}
}
function validateSampleRate(sampleRate) {
if (!sampleRate) {
return SAMPLE_RATES.SPEECH;
}
if (sampleRate < SAMPLE_RATES.SPEECH) {
console.warn(`采样率 ${sampleRate}Hz 低于最小值,已调整为 ${SAMPLE_RATES.SPEECH}Hz`);
return SAMPLE_RATES.SPEECH;
}
if (sampleRate > SAMPLE_RATES.HD) {
console.warn(`采样率 ${sampleRate}Hz 高于最大值,已调整为 ${SAMPLE_RATES.HD}Hz`);
return SAMPLE_RATES.HD;
}
return sampleRate;
}
async function createRecorder(options = {}) {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new AudioProcessingError("浏览器不支持录音功能");
}
const validatedSampleRate = validateSampleRate(options.sampleRate);
const isStreaming = options.isStreaming !== void 0 ? options.isStreaming : !!(options.onDataAvailable || options.timeslice);
const config = {
format: options.format || AUDIO_FORMATS.WEBM,
sampleRate: validatedSampleRate,
channelCount: options.channelCount || CHANNEL_COUNTS.MONO,
bitRate: options.bitRate || 128,
echoCancellation: options.echoCancellation !== void 0 ? options.echoCancellation : true,
noiseSuppression: options.noiseSuppression !== void 0 ? options.noiseSuppression : true,
autoGainControl: options.autoGainControl !== void 0 ? options.autoGainControl : true,
timeslice: options.timeslice || 100,
onDataAvailable: options.onDataAvailable,
onAudioProcess: options.onAudioProcess,
isStreaming
};
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: config.echoCancellation,
noiseSuppression: config.noiseSuppression,
autoGainControl: config.autoGainControl,
channelCount: config.channelCount
}
});
} catch (error) {
console.error("获取麦克风权限失败:", error);
throw new AudioProcessingError(`无法访问麦克风: ${error instanceof Error ? error.message : String(error)}`);
}
let audioContext = null;
let audioSource = null;
let analyser = null;
let dataArray = new Uint8Array(0);
let analyzerConnected = false;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const actualSampleRate = audioContext.sampleRate;
audioSource = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
dataArray = new Uint8Array(analyser.frequencyBinCount);
audioSource.connect(analyser);
analyzerConnected = true;
if (config.sampleRate !== actualSampleRate) {
console.info(`系统采样率为 ${actualSampleRate}Hz,将在录音结束后重采样为 ${config.sampleRate}Hz`);
}
} catch (error) {
console.warn("初始化音频分析器失败:", error);
releaseAudioResources(audioContext, audioSource, analyser);
audioContext = null;
audioSource = null;
analyser = null;
analyzerConnected = false;
}
const mimeType = getSupportedMimeType(config.format);
let mediaRecorder;
try {
mediaRecorder = new MediaRecorder(stream, {
mimeType,
audioBitsPerSecond: config.bitRate * 1e3
});
} catch (error) {
console.warn(`创建MediaRecorder失败,使用默认配置: ${error}`);
try {
mediaRecorder = new MediaRecorder(stream);
} catch (fallbackError) {
releaseAudioResources(audioContext, audioSource, analyser);
stream.getTracks().forEach((track) => track.stop());
throw new AudioProcessingError(`无法创建录音器: ${fallbackError}`);
}
}
let chunks = [];
let startTime = 0;
let pausedTime = 0;
let totalPausedTime = 0;
let audioProcessInterval = null;
mediaRecorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) {
chunks.push(e.data);
if (config.isStreaming && config.onDataAvailable) {
config.onDataAvailable(e.data);
}
}
};
const recorder = {
async start() {
if (mediaRecorder.state === "inactive") {
chunks = [];
startTime = Date.now();
totalPausedTime = 0;
const timeslice = config.isStreaming ? config.timeslice : 1e3;
mediaRecorder.start(timeslice);
if (config.onAudioProcess) {
startAudioProcessing(config.onAudioProcess, analyser, analyzerConnected, dataArray);
}
}
},
pause() {
if (mediaRecorder.state === "recording") {
mediaRecorder.pause();
pausedTime = Date.now();
stopAudioProcessing();
}
},
resume() {
if (mediaRecorder.state === "paused") {
mediaRecorder.resume();
totalPausedTime += Date.now() - pausedTime;
if (config.onAudioProcess && audioProcessInterval === null) {
startAudioProcessing(config.onAudioProcess, analyser, analyzerConnected, dataArray);
}
}
},
async stop() {
return new Promise((resolve) => {
const handleStop = async () => {
stopAudioProcessing();
if (chunks.length === 0) {
resolve(new Blob([], { type: mimeType }));
return;
}
const rawBlob = new Blob(chunks, { type: mimeType });
try {
const processedBlob = await processAudio(rawBlob, {
format: config.format,
sampleRate: config.sampleRate,
channelCount: config.channelCount,
bitRate: config.bitRate
});
resolve(processedBlob);
} catch (error) {
console.error("音频处理失败:", error);
resolve(rawBlob);
}
};
if (mediaRecorder.state !== "inactive") {
mediaRecorder.onstop = handleStop;
if ("requestData" in mediaRecorder && mediaRecorder.state === "recording") {
try {
mediaRecorder.requestData();
} catch (e) {
}
}
mediaRecorder.stop();
} else {
handleStop();
}
});
},
cancel() {
if (mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
}
stopAudioProcessing();
chunks = [];
},
getState() {
return mediaRecorder.state;
},
getDuration() {
if (startTime === 0) return 0;
if (mediaRecorder.state === "paused") {
return (pausedTime - startTime - totalPausedTime) / 1e3;
}
return (Date.now() - startTime - totalPausedTime) / 1e3;
},
getAudioData() {
if (!analyzerConnected || !analyser) {
return new Uint8Array(0);
}
try {
analyser.getByteFrequencyData(dataArray);
return dataArray.slice(0);
} catch (e) {
return new Uint8Array(0);
}
},
async getCurrentAudio() {
if (chunks.length === 0) {
return new Blob([], { type: mimeType });
}
const rawBlob = new Blob(chunks.slice(0), { type: mimeType });
try {
const processedBlob = await processAudio(rawBlob, {
format: config.format,
sampleRate: config.sampleRate,
channelCount: config.channelCount,
bitRate: config.bitRate
});
return processedBlob;
} catch (error) {
console.error("处理当前音频失败:", error);
return rawBlob;
}
},
release() {
stopAudioProcessing();
releaseAudioResources(audioContext, audioSource, analyser);
stream.getTracks().forEach((track) => track.stop());
}
};
function startAudioProcessing(callback, analyser2, connected, dataArray2) {
if (audioProcessInterval !== null) {
clearInterval(audioProcessInterval);
}
audioProcessInterval = window.setInterval(() => {
if (connected && analyser2) {
try {
analyser2.getByteFrequencyData(dataArray2);
callback(dataArray2.slice(0));
} catch (e) {
callback(new Uint8Array(0));
}
} else {
callback(new Uint8Array(0));
}
}, 50);
}
function stopAudioProcessing() {
if (audioProcessInterval !== null) {
clearInterval(audioProcessInterval);
audioProcessInterval = null;
}
}
return recorder;
}
function releaseAudioResources(context, source, analyser) {
if (analyser) {
try {
analyser.disconnect();
} catch (e) {
}
}
if (source) {
try {
source.disconnect();
} catch (e) {
}
}
if (context) {
try {
context.close();
} catch (e) {
}
}
}
function getSupportedMimeType(format) {
const preferredType = getFormatMimeType(format);
if (MediaRecorder.isTypeSupported(preferredType)) {
return preferredType;
}
const fallbackTypes = [
"audio/webm;codecs=opus",
"audio/webm",
"audio/ogg;codecs=opus",
"audio/mp4",
"audio/mpeg",
""
// 空字符串表示浏览器默认
];
for (const type of fallbackTypes) {
if (type === "" || MediaRecorder.isTypeSupported(type)) {
console.warn(`格式 ${format} 不受支持,使用 ${type || "浏览器默认"} 替代`);
return type;
}
}
return "";
}
function getFormatMimeType(format) {
const mimeTypes = {
[AUDIO_FORMATS.WAV]: "audio/wav",
[AUDIO_FORMATS.MP3]: "audio/mpeg",
[AUDIO_FORMATS.OGG]: "audio/ogg;codecs=opus",
[AUDIO_FORMATS.WEBM]: "audio/webm;codecs=opus"
};
return mimeTypes[format] || "audio/webm;codecs=opus";
}
async function processAudio(blob, options) {
const { format, sampleRate, channelCount, bitRate } = options;
const validatedSampleRate = validateSampleRate(sampleRate);
const audioContext = new AudioContext();
try {
const arrayBuffer = await blob.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const originalSampleRate = audioBuffer.sampleRate;
const originalChannels = audioBuffer.numberOfChannels;
let processedBuffer = audioBuffer;
if (validatedSampleRate !== originalSampleRate) {
processedBuffer = await resampleAudio(
audioBuffer,
validatedSampleRate,
originalChannels
);
}
if (channelCount !== originalChannels) {
processedBuffer = adjustChannels(
processedBuffer,
channelCount,
audioContext
);
}
let outputBlob;
if (format === AUDIO_FORMATS.WAV) {
outputBlob = encodeToWav(processedBuffer);
} else {
outputBlob = await encodeWithMediaRecorder(
processedBuffer,
format,
bitRate
);
}
audioContext.close();
return outputBlob;
} catch (error) {
audioContext.close();
console.error("音频处理失败:", error);
throw new AudioProcessingError(`音频处理失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function resampleAudio(buffer, targetSampleRate, channels) {
const duration = buffer.duration;
const newLength = Math.ceil(duration * targetSampleRate);
const offlineContext = new OfflineAudioContext(
channels,
newLength,
targetSampleRate
);
const source = offlineContext.createBufferSource();
source.buffer = buffer;
source.connect(offlineContext.destination);
source.start(0);
const resampledBuffer = await offlineContext.startRendering();
console.info(`重采样完成: ${buffer.sampleRate}Hz -> ${targetSampleRate}Hz`);
return resampledBuffer;
}
function adjustChannels(buffer, targetChannels, context) {
const sourceChannels = buffer.numberOfChannels;
if (sourceChannels === targetChannels) {
return buffer;
}
const newBuffer = context.createBuffer(
targetChannels,
buffer.length,
buffer.sampleRate
);
if (targetChannels === 1 && sourceChannels > 1) {
const output = newBuffer.getChannelData(0);
for (let i = 0; i < buffer.length; i++) {
let sum = 0;
for (let channel = 0; channel < sourceChannels; channel++) {
sum += buffer.getChannelData(channel)[i];
}
output[i] = sum / sourceChannels;
}
} else if (targetChannels > sourceChannels) {
for (let channel = 0; channel < targetChannels; channel++) {
const output = newBuffer.getChannelData(channel);
const sourceChannel = channel % sourceChannels;
output.set(buffer.getChannelData(sourceChannel));
}
} else {
for (let channel = 0; channel < targetChannels; channel++) {
newBuffer.copyToChannel(buffer.getChannelData(channel), channel);
}
}
console.info(`通道调整完成: ${sourceChannels} -> ${targetChannels}`);
return newBuffer;
}
function encodeToWav(buffer) {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const channels = [];
for (let i = 0; i < numChannels; i++) {
channels.push(buffer.getChannelData(i));
}
let interleaved;
if (numChannels === 2) {
interleaved = interleaveChannels(channels[0], channels[1]);
} else if (numChannels === 1) {
interleaved = channels[0];
} else {
interleaved = new Float32Array(buffer.length * numChannels);
for (let i = 0; i < buffer.length; i++) {
for (let channel = 0; channel < numChannels; channel++) {
interleaved[i * numChannels + channel] = channels[channel][i];
}
}
}
const wavData = createWavHeader(interleaved, {
sampleRate,
numChannels,
bitsPerSample: 16
});
return new Blob([wavData], { type: "audio/wav" });
}
async function encodeWithMediaRecorder(buffer, format, bitRate) {
const context = new AudioContext();
const source = context.createBufferSource();
source.buffer = buffer;
const destination = context.createMediaStreamDestination();
source.connect(destination);
const mimeType = getSupportedMimeType(format);
const options = {};
if (mimeType) {
options.mimeType = mimeType;
}
if (bitRate) {
options.audioBitsPerSecond = bitRate * 1e3;
}
let recorder;
try {
recorder = new MediaRecorder(destination.stream, options);
} catch (error) {
console.warn(`创建编码器失败,使用默认设置: ${error}`);
recorder = new MediaRecorder(destination.stream);
}
const chunks = [];
recorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) {
chunks.push(e.data);
}
};
return new Promise((resolve, reject) => {
recorder.onstop = () => {
source.disconnect();
context.close();
if (chunks.length === 0) {
reject(new Error("编码过程中没有数据生成"));
return;
}
const finalBlob = new Blob(chunks, { type: mimeType || `audio/${format}` });
resolve(finalBlob);
};
recorder.start();
source.start(0);
const duration = buffer.duration * 1e3;
setTimeout(() => {
try {
recorder.stop();
} catch (error) {
reject(error);
}
}, duration + 500);
});
}
function interleaveChannels(left, right) {
const length = left.length + right.length;
const result = new Float32Array(length);
let inputIndex = 0;
for (let i = 0; i < length; ) {
result[i++] = left[inputIndex];
result[i++] = right[inputIndex];
inputIndex++;
}
return result;
}
function createWavHeader(samples, options) {
const { sampleRate, numChannels, bitsPerSample } = options;
const bytesPerSample = bitsPerSample / 8;
const blockAlign = numChannels * bytesPerSample;
const dataLength = samples.length * bytesPerSample;
const buffer = new ArrayBuffer(44 + dataLength);
const view = new DataView(buffer);
writeString(view, 0, "RIFF");
view.setUint32(4, 36 + dataLength, true);
writeString(view, 8, "WAVE");
writeString(view, 12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * blockAlign, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitsPerSample, true);
writeString(view, 36, "data");
view.setUint32(40, dataLength, true);
if (bitsPerSample === 16) {
floatToInt16(view, 44, samples);
} else if (bitsPerSample === 8) {
floatToInt8(view, 44, samples);
}
return view;
}
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function floatToInt16(view, offset, samples) {
for (let i = 0; i < samples.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, samples[i]));
view.setInt16(offset, s < 0 ? s * 32768 : s * 32767, true);
}
}
function floatToInt8(view, offset, samples) {
for (let i = 0; i < samples.length; i++, offset++) {
const s = Math.max(-1, Math.min(1, samples[i]));
const val = s < 0 ? s * 128 : s * 127;
view.setInt8(offset, val + 128);
}
}
async function blobToAudioBuffer(blob, context) {
const audioContext = context || new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await blob.arrayBuffer();
return await audioContext.decodeAudioData(arrayBuffer);
}
function playAudioBlob(blob) {
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.onended = () => {
URL.revokeObjectURL(url);
};
audio.play();
return audio;
}
function base64ToAudioBlob(base64, mimeType) {
const base64Data = base64.includes(",") ? base64.split(",")[1] : base64;
try {
const byteString = atob(base64Data);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
return new Blob([arrayBuffer], { type: mimeType });
} catch (error) {
console.error("Base64转换失败:", error);
throw new AudioProcessingError("无效的Base64音频数据");
}
}
function audioBlobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = (error) => reject(new AudioProcessingError(`转换Blob到Base64失败: ${error}`));
reader.readAsDataURL(blob);
});
}
async function streamToAudioBlob(stream, mimeType) {
try {
const bodyStream = stream instanceof Response ? stream.body : stream;
if (!bodyStream) {
throw new Error("无效的流数据");
}
const reader = bodyStream.getReader();
const chunks = [];
let totalLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLength += value.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
if (!mimeType) {
mimeType = detectAudioMimeType(result);
}
return new Blob([result], { type: mimeType });
} catch (error) {
console.error("转换音频流到 Blob 失败:", error);
throw error;
}
}
function detectAudioMimeType(data) {
if (data.length > 12 && data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 && data[8] === 87 && data[9] === 65 && data[10] === 86 && data[11] === 69) {
return "audio/wav";
}
if (data.length > 3 && (data[0] === 73 && data[1] === 68 && data[2] === 51 || // ID3
data[0] === 255 && (data[1] & 224) === 224)) {
return "audio/mp3";
}
return "audio/mp3";
}
async function playAudioStream(stream, mimeType) {
const blob = await streamToAudioBlob(stream, mimeType);
return playAudioBlob(blob);
}
async function createStreamingAudioPlayer(stream, mimeType, options = {}) {
const defaultOptions = {
autoPlay: false,
volume: 1,
loop: false,
bufferSize: 3,
// 秒
useMediaSource: true,
playWhenEnoughData: true,
bufferUpdateInterval: 250
// 毫秒
};
const config = { ...defaultOptions, ...options };
let bodyStream = stream instanceof Response ? stream.body : stream;
if (!bodyStream) {
throw new Error("无效的流数据");
}
let actualMimeType = mimeType;
let initialData = null;
if (!actualMimeType) {
const reader = bodyStream.getReader();
const { value, done } = await reader.read();
if (done || !value) {
throw new Error("流为空或已结束");
}
initialData = value;
actualMimeType = detectAudioMimeType(initialData);
const remainingStream = new ReadableStream({
start(controller) {
controller.enqueue(initialData);
},
async pull(controller) {
try {
const { value: value2, done: done2 } = await reader.read();
if (done2) {
controller.close();
} else {
controller.enqueue(value2);
}
} catch (error) {
controller.error(error);
}
}
});
bodyStream = remainingStream;
}
const isMP3 = actualMimeType.includes("mp3") || actualMimeType.includes("mpeg");
const isWebM = actualMimeType.includes("webm");
const isMP4 = actualMimeType.includes("mp4");
const canUseMediaSource = (isMP3 || isWebM || isMP4) && config.useMediaSource && typeof MediaSource !== "undefined";
if (canUseMediaSource) {
return createMediaSourcePlayer(bodyStream, actualMimeType, config);
} else {
return createAudioElementPlayer(bodyStream, actualMimeType, config);
}
}
async function createMediaSourcePlayer(stream, mimeType, options) {
let sourceType = mimeType;
if (mimeType.includes("mp3") || mimeType.includes("mpeg")) {
sourceType = "audio/mpeg";
} else if (mimeType.includes("wav")) {
throw new Error("MediaSource API 不支持 WAV 格式,请使用 MP3/WebM/MP4 格式");
}
const mediaSource = new MediaSource();
const sourceUrl = URL.createObjectURL(mediaSource);
const audioElement = new Audio();
audioElement.volume = options.volume;
audioElement.loop = options.loop;
audioElement.src = sourceUrl;
let sourceBuffer = null;
let isPlaying = false;
let isPaused = false;
let isStopped = true;
let enqueuedChunks = [];
let isBufferUpdating = false;
let reader = null;
let initialSegmentAppended = false;
let bufferUpdateIntervalId = null;
let bufferedProgress = 0;
let onEndedCallback = null;
let onBufferUpdateCallback = null;
let onTimeUpdateCallback = null;
let onErrorCallback = null;
await new Promise((resolve) => {
mediaSource.addEventListener("sourceopen", () => {
try {
sourceBuffer = mediaSource.addSourceBuffer(sourceType);
sourceBuffer.addEventListener("updateend", processQueue);
resolve();
} catch (error) {
onError(new Error(`创建 SourceBuffer 失败: ${error}`));
resolve();
}
}, { once: true });
});
if (!sourceBuffer) {
URL.revokeObjectURL(sourceUrl);
return createAudioElementPlayer(stream, mimeType, options);
}
const startBufferUpdateInterval = () => {
if (bufferUpdateIntervalId !== null) {
clearInterval(bufferUpdateIntervalId);
}
bufferUpdateIntervalId = window.setInterval(() => {
if (audioElement && sourceBuffer) {
updateBufferedProgress();
}
}, options.bufferUpdateInterval);
};
const updateBufferedProgress = () => {
if (!audioElement || audioElement.duration === Infinity || isNaN(audioElement.duration)) {
return;
}
const timeRanges = audioElement.buffered;
if (timeRanges.length > 0) {
const bufferedEnd = timeRanges.end(timeRanges.length - 1);
bufferedProgress = Math.min(1, bufferedEnd / audioElement.duration);
if (onBufferUpdateCallback) {
onBufferUpdateCallback(bufferedProgress);
}
}
};
const processQueue = async () => {
if (!sourceBuffer || isBufferUpdating || enqueuedChunks.length === 0) {
return;
}
isBufferUpdating = true;
try {
const chunk = enqueuedChunks.shift();
sourceBuffer.appendBuffer(chunk);
if (!initialSegmentAppended) {
initialSegmentAppended = true;
if (options.autoPlay && options.playWhenEnoughData) {
playAudio();
}
}
updateBufferedProgress();
} catch (error) {
onError(new Error(`缓冲数据失败: ${error}`));
}
};
const startStreaming = async () => {
try {
reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
if (mediaSource.readyState === "open") {
try {
mediaSource.endOfStream();
} catch (error) {
console.warn("媒体源关闭失败:", error);
}
}
break;
}
enqueuedChunks.push(value);
if (!isBufferUpdating) {
processQueue();
}
}
} catch (error) {
onError(new Error(`流读取失败: ${error}`));
}
};
const playAudio = () => {
if (!isPlaying) {
const playPromise = audioElement.play();
if (playPromise !== void 0) {
playPromise.catch((error) => {
console.warn("自动播放失败:", error);
});
}
isPlaying = true;
isPaused = false;
isStopped = false;
}
};
const onError = (error) => {
if (onErrorCallback) {
onErrorCallback(error);
} else {
console.error("流式音频播放器错误:", error);
}
};
audioElement.addEventListener("ended", () => {
isPlaying = false;
if (onEndedCallback) {
onEndedCallback();
}
});
audioElement.addEventListener("timeupdate", () => {
if (onTimeUpdateCallback) {
onTimeUpdateCallback(audioElement.currentTime, audioElement.duration || 0);
}
});
audioElement.addEventListener("error", () => {
var _a;
onError(new Error(`音频元素错误: ${((_a = audioElement.error) == null ? void 0 : _a.message) || "未知错误"}`));
});
startBufferUpdateInterval();
startStreaming();
const player = {
play() {
playAudio();
},
pause() {
if (isPlaying && !isPaused) {
audioElement.pause();
isPaused = true;
isPlaying = false;
}
},
resume() {
if (isPaused) {
playAudio();
}
},
stop() {
if (!isStopped) {
audioElement.pause();
audioElement.currentTime = 0;
isPlaying = false;
isPaused = false;
isStopped = true;
if (reader) {
try {
reader.cancel();
} catch (e) {
}
reader = null;
}
if (bufferUpdateIntervalId !== null) {
clearInterval(bufferUpdateIntervalId);
bufferUpdateIntervalId = null;
}
URL.revokeObjectURL(sourceUrl);
}
},
setVolume(volume) {
audioElement.volume = Math.max(0, Math.min(1, volume));
},
getState() {
if (isPlaying) return "playing";
if (isPaused) return "paused";
return "stopped";
},
getDuration() {
return isNaN(audioElement.duration) ? 0 : audioElement.duration;
},
getCurrentTime() {
return audioElement.currentTime;
},
seek(time) {
if (!isNaN(audioElement.duration) && time >= 0 && time <= audioElement.duration) {
audioElement.currentTime = time;
}
},
getBufferedProgress() {
return bufferedProgress;
},
onEnded: null,
onBufferUpdate: null,
onTimeUpdate: null,
onError: null
};
Object.defineProperties(player, {
onEnded: {
get: () => onEndedCallback,
set: (callback) => {
onEndedCallback = callback;
}
},
onBufferUpdate: {
get: () => onBufferUpdateCallback,
set: (callback) => {
onBufferUpdateCallback = callback;
}
},
onTimeUpdate: {
get: () => onTimeUpdateCallback,
set: (callback) => {
onTimeUpdateCallback = callback;
}
},
onError: {
get: () => onErrorCallback,
set: (callback) => {
onErrorCallback = callback;
}
}
});
if (options.autoPlay && !options.playWhenEnoughData) {
playAudio();
}
return player;
}
async function createAudioElementPlayer(stream, mimeType, options) {
let audioElement = new Audio();
let isPlaying = false;
let isPaused = false;
let isStopped = true;
let bufferedProgress = 0;
let bufferUpdateIntervalId = null;
let audioBlob = null;
let audioUrl = null;
let onEndedCallback = null;
let onBufferUpdateCallback = null;
let onTimeUpdateCallback = null;
let onErrorCallback = null;
let hasStartedPlaying = false;
let dataChunks = [];
let totalBytesReceived = 0;
const targetBufferSize = options.bufferSize * 128 * 1024;
const onError = (error) => {
if (onErrorCallback) {
onErrorCallback(error);
} else {
console.error("流式音频播放器错误:", error);
}
};
const reader = stream.getReader();
const readChunks = async () => {
try {
const { value, done } = await reader.read();
if (done) {
createAndPlayFinalAudio();
return true;
}
dataChunks.push(value);
totalBytesReceived += value.length;
updateBufferedProgress(totalBytesReceived / (totalBytesReceived + 512 * 1024));
if (!hasStartedPlaying && options.autoPlay && options.playWhenEnoughData && totalBytesReceived >= targetBufferSize) {
createAndPlayIntermediateAudio();
}
return false;
} catch (error) {
onError(new Error(`读取流失败: ${error}`));
return true;
}
};
const startBufferUpdateInterval = () => {
if (bufferUpdateIntervalId !== null) {
clearInterval(bufferUpdateIntervalId);
}
bufferUpdateIntervalId = window.setInterval(() => {
if (audioElement) {
updateActualBufferedProgress();
}
}, options.bufferUpdateInterval);
};
const updateActualBufferedProgress = () => {
if (!audioElement || audioElement.duration === Infinity || isNaN(audioElement.duration)) {
return;
}
const timeRanges = audioElement.buffered;
if (timeRanges.length > 0) {
const bufferedEnd = timeRanges.end(timeRanges.length - 1);
bufferedProgress = Math.min(1, bufferedEnd / audioElement.duration);
if (onBufferUpdateCallback) {
onBufferUpdateCallback(bufferedProgress);
}
}
};
const updateBufferedProgress = (progress) => {
bufferedProgress = progress;
if (onBufferUpdateCallback) {
onBufferUpdateCallback(bufferedProgress);
}
};
const createAndPlayIntermediateAudio = () => {
hasStartedPlaying = true;
const combinedData = concatenateUint8Arrays(dataChunks);
const tempBlob = new Blob([combinedData], { type: mimeType });
if (audioElement) {
const tempUrl = URL.createObjectURL(tempBlob);
audioElement.src = tempUrl;
audioElement.oncanplaythrough = () => {
playAudio();
audioElement.oncanplaythrough = null;
};
setupAudioEvents(audioElement);
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
audioUrl = tempUrl;
}
};
const createAndPlayFinalAudio = () => {
const combinedData = concatenateUint8Arrays(dataChunks);
audioBlob = new Blob([combinedData], { type: mimeType });
if (audioElement) {
const currentTime = audioElement.currentTime;
const wasPlaying = isPlaying && !isPaused;
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
audioUrl = URL.createObjectURL(audioBlob);
audioElement.src = audioUrl;
audioElement.currentTime = currentTime;
setupAudioEvents(audioElement);
if (wasPlaying) {
playAudio();
}
}
updateBufferedProgress(1);
};
const setupAudioEvents = (audio) => {
audio.addEventListener("ended", () => {
isPlaying = false;
if (onEndedCallback) {
onEndedCallback();
}
});
audio.addEventListener("timeupdate", () => {
if (onTimeUpdateCallback) {
onTimeUpdateCallback(audio.currentTime, audio.duration || 0);
}
});
audio.addEventListener("error", () => {
var _a;
onError(new Error(`音频元素错误: ${((_a = audio.error) == null ? void 0 : _a.message) || "未知错误"}`));
});
};
const concatenateUint8Arrays = (arrays) => {
let totalLength = 0;
arrays.forEach((arr) => {
totalLength += arr.length;
});
const result = new Uint8Array(totalLength);
let offset = 0;
arrays.forEach((arr) => {
result.set(arr, offset);
offset += arr.length;
});
return result;
};
const startStreamReading = async () => {
let completed = false;
while (!completed) {
completed = await readChunks();
}
};
const playAudio = () => {
if (!isPlaying && audioElement) {
const playPromise = audioElement.play();
if (playPromise !== void 0) {
playPromise.catch((error) => {
console.warn("自动播放失败:", error);
});
}
isPlaying = true;
isPaused = false;
isStopped = false;
}
};
if (audioElement) {
audioElement.volume = options.volume;
audioElement.loop = options.loop;
setupAudioEvents(audioElement);
}
startBufferUpdateInterval();
startStreamReading();
if (options.autoPlay && !options.playWhenEnoughData) {
const silentBlob = new Blob([new Uint8Array(1)], { type: mimeType });
if (audioElement) {
audioUrl = URL.createObjectURL(silentBlob);
audioElement.src = audioUrl;
playAudio();
}
}
const player = {
play() {
if (dataChunks.length > 0 && !isPlaying) {
if (!hasStartedPlaying) {
createAndPlayIntermediateAudio();
} else {
playAudio();
}
}
},
pause() {
if (isPlaying && !isPaused && audioElement) {
audioElement.pause();
isPaused = true;
isPlaying = false;
}
},
resume() {
if (isPaused) {
playAudio();
}
},
stop() {
if (!isStopped && audioElement) {
audioElement.pause();
audioElement.currentTime = 0;
isPlaying = false;
isPaused = false;
isStopped = true;
if (bufferUpdateIntervalId !== null) {
clearInterval(bufferUpdateIntervalId);
bufferUpdateIntervalId = null;
}
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
audioUrl = null;
}
try {
reader.cancel();
} catch (e) {
}
}
},
setVolume(volume) {
if (audioElement) {
audioElement.volume = Math.max(0, Math.min(1, volume));
}
},
getState() {
if (isPlaying) return "playing";
if (isPaused) return "paused";
return "stopped";
},
getDuration() {
return audioElement && !isNaN(audioElement.duration) ? audioElement.duration : 0;
},
getCurrentTime() {
return audioElement ? audioElement.currentTime : 0;
},
seek(time) {
if (audioElement && !isNaN(audioElement.duration) && time >= 0 && time <= audioElement.duration) {
audioElement.currentTime = time;
}
},
getBufferedProgress() {
return bufferedProgress;
},
onEnded: null,
onBufferUpdate: null,
onTimeUpdate: null,
onError: null
};
Object.defineProperties(player, {
onEnded: {
get: () => onEndedCallback,
set: (callback) => {
onEndedCallback = callback;
}
},
onBufferUpdate: {
get: () => onBufferUpdateCallback,
set: (callback) => {
onBufferUpdateCallback = callback;
}
},
onTimeUpdate: {
get: () => onTimeUpdateCallback,
set: (callback) => {
onTimeUpdateCallback = callback;
}
},
onError: {
get: () => onErrorCallback,
set: (callback) => {
onErrorCallback = callback;
}
}
});
return player;
}
export {
AUDIO_FORMATS,
CHANNEL_COUNTS,
SAMPLE_RATES,
audioBlobToBase64,
base64ToAudioBlob,
blobToAudioBuffer,
createRecorder,
createStreamingAudioPlayer,
playAudioBlob,
playAudioStream,
streamToAudioBlob
};