音频录制sdk开发

class VoiceRecorder {

/**

* @param {Object} options

* @param {HTMLElement} options.el 录音按钮DOM

* @param {number} options.minDuration 最小录音时长,单位ms,默认500

* @param {number} options.maxDuration 最大录音时长,单位ms,默认60000

* @param {Function} options.onStart 开始录音

* @param {Function} options.onStop 停止录音

* @param {Function} options.onComplete 录音完成,返回Blob

* @param {Function} options.onError 错误

* @param {Function} options.onDuration 录音时长变化

*/

constructor(options = {}) {

this.el = options.el;

if (!(this.el instanceof HTMLElement)) {

throw new Error("VoiceRecorder el必须是HTMLElement");

}

this.minDuration = options.minDuration ?? 500;

this.maxDuration = options.maxDuration ?? 60000;

this.onStart = options.onStart || (() => {});

this.onStop = options.onStop || (() => {});

this.onComplete = options.onComplete || (() => {});

this.onError = options.onError || ((error) => {

console.error("VoiceRecorder", error);

});

this.onDuration = options.onDuration || (() => {});

// MediaStream

this.stream = null;

// MediaRecorder

this.mediaRecorder = null;

// 音频数据

this.chunks = \[\];

// 状态

this.isRecording = false;

// 当前录音开始时间

this.startTime = 0;

// 最大录音时长定时器

this.maxDurationTimer = null;

// 是否正在销毁

this.destroyed = false;

// 防止重复处理

this.isStopping = false;

// 当前录音格式

this.mimeType = "";

// 事件绑定

this.bindEvents();

}

/**

* ==============================

* 静态能力检测

* ==============================

*/

static checkSupport() {

const result = {

supported: true,

secureContext: window.isSecureContext,

mediaDevices: !!navigator.mediaDevices,

getUserMedia: !!(

navigator.mediaDevices &&

navigator.mediaDevices.getUserMedia

),

mediaRecorder: typeof MediaRecorder !== "undefined",

mimeType: null,

reason: ""

};

// HTTPS / localhost

if (!result.secureContext) {

result.supported = false;

result.reason = "当前页面不是安全上下文,请使用HTTPS访问";

return result;

}

// MediaDevices

if (!result.mediaDevices) {

result.supported = false;

result.reason = "当前浏览器不支持navigator.mediaDevices";

return result;

}

// getUserMedia

if (!result.getUserMedia) {

result.supported = false;

result.reason = "当前浏览器不支持getUserMedia";

return result;

}

// MediaRecorder

if (!result.mediaRecorder) {

result.supported = false;

result.reason = "当前浏览器不支持MediaRecorder";

return result;

}

// 音频格式

result.mimeType = VoiceRecorder.getSupportedMimeType();

if (!result.mimeType) {

result.supported = false;

result.reason = "当前浏览器没有找到支持的音频录制格式";

return result;

}

return result;

}

/**

* 获取浏览器支持的音频录制格式

*

* 优先级:

* MP4 > WebM > OGG

*/

static getSupportedMimeType() {

if (typeof MediaRecorder === "undefined") {

return "";

}

const types = [

"audio/mp4",

"audio/webm;codecs=opus",

"audio/webm",

"audio/ogg;codecs=opus"

];

for (const type of types) {

try {

if (MediaRecorder.isTypeSupported(type)) {

return type;

}

} catch (error) {

console.warn(

"VoiceRecorder MIME检测失败:",

type,

error

);

}

}

return "";

}

/**

* ==============================

* 初始化

* ==============================

*/

bindEvents() {

/**

* 使用Pointer Events统一处理:

*

* PC mouse

* 手机 touch

* stylus

*

* 避免同时维护mousedown/touchstart。

*/

this.el.addEventListener(

"pointerdown",

this.handlePointerDown

);

this.el.addEventListener(

"pointerup",

this.handlePointerUp

);

this.el.addEventListener(

"pointercancel",

this.handlePointerCancel

);

this.el.addEventListener(

"pointerleave",

this.handlePointerLeave

);

// 防止移动端长按弹出菜单

this.el.style.touchAction = "none";

this.el.style.userSelect = "none";

}

/**

* ==============================

* 开始录音

* ==============================

*/

handlePointerDown = async (event) => {

event.preventDefault();

if (this.destroyed) {

return;

}

if (this.isRecording || this.isStopping) {

return;

}

// 确保pointer事件仍然绑定在当前元素

try {

this.el.setPointerCapture(event.pointerId);

} catch (error) {

// 部分环境可能不支持,忽略

}

await this.start();

};

/**

* ==============================

* 停止录音

* ==============================

*/

handlePointerUp = (event) => {

event.preventDefault();

this.stop();

};

handlePointerCancel = () => {

this.stop();

};

handlePointerLeave = () => {

/**

* 这里不直接停止。

*

* 因为移动端/PC拖动手指或鼠标离开元素时,

* 用户可能仍然希望继续录音。

*

* 真正结束以pointerup/pointercancel为准。

*/

};

/**

* ==============================

* 开始录音

* ==============================

*/

async start() {

if (this.destroyed) {

throw new Error("VoiceRecorder 实例已经销毁");

}

if (this.isRecording) {

return;

}

// 前置能力检测

const support = VoiceRecorder.checkSupport();

if (!support.supported) {

const error = new Error(support.reason);

this.onError(error);

return;

}

try {

// 获取麦克风

if (!this.stream) {

this.stream =

await navigator.mediaDevices.getUserMedia({

audio: true,

video: false

});

// 监听麦克风Track结束

this.bindTrackEvents();

}

// 如果流已经失效,重新获取

if (!this.isStreamActive()) {

this.releaseStream();

this.stream =

await navigator.mediaDevices.getUserMedia({

audio: true,

video: false

});

this.bindTrackEvents();

}

this.chunks = \[\];

this.isStopping = false;

this.mimeType =

VoiceRecorder.getSupportedMimeType();

const recorderOptions = {};

if (this.mimeType) {

recorderOptions.mimeType = this.mimeType;

}

this.mediaRecorder =

new MediaRecorder(

this.stream,

recorderOptions

);

// 音频数据

this.mediaRecorder.ondataavailable =

(event) => {

if (

event.data &&

event.data.size > 0

) {

this.chunks.push(event.data);

}

};

// 录音停止

this.mediaRecorder.onstop =

() => {

this.handleRecorderStop();

};

// 录音异常

this.mediaRecorder.onerror =

(event) => {

this.isRecording = false;

this.isStopping = false;

this.clearMaxDurationTimer();

this.onError(

event.error ||

new Error("录音发生异常")

);

};

// 开始录音

this.mediaRecorder.start();

this.isRecording = true;

this.startTime = Date.now();

// 最大录音时间

this.maxDurationTimer =

setTimeout(() => {

if (this.isRecording) {

this.stop();

}

}, this.maxDuration);

this.onStart();

} catch (error) {

this.isRecording = false;

this.isStopping = false;

this.clearMaxDurationTimer();

this.onError(

this.normalizeError(error)

);

}

}

/**

* ==============================

* 停止录音

* ==============================

*/

stop() {

if (!this.isRecording) {

return;

}

if (!this.mediaRecorder) {

return;

}

if (

this.mediaRecorder.state === "inactive"

) {

return;

}

this.isStopping = true;

this.clearMaxDurationTimer();

try {

this.mediaRecorder.stop();

this.isRecording = false;

this.onStop();

} catch (error) {

this.isRecording = false;

this.isStopping = false;

this.onError(

this.normalizeError(error)

);

}

}

/**

* ==============================

* MediaRecorder停止后的处理

* ==============================

*/

handleRecorderStop() {

const duration =

Date.now() - this.startTime;

this.isRecording = false;

this.isStopping = false;

this.clearMaxDurationTimer();

// 没有录到数据

if (!this.chunks.length) {

this.onError(

new Error("未获取到录音数据")

);

return;

}

// 录音时间太短

if (duration < this.minDuration) {

this.chunks = \[\];

this.onError(

new Error(

`录音时间过短,至少需要${this.minDuration}ms`

)

);

return;

}

const mimeType =

this.mediaRecorder?.mimeType ||

this.chunks0?.type ||

this.mimeType ||

"audio/webm";

const blob = new Blob(

this.chunks,

{

type: mimeType

}

);

const result = {

blob,

type: blob.type,

size: blob.size,

duration

};

// 清空chunks

this.chunks = \[\];

// 最终结果

this.onComplete(result);

}

/**

* ==============================

* 检查Stream是否有效

* ==============================

*/

isStreamActive() {

if (!this.stream) {

return false;

}

const tracks =

this.stream.getAudioTracks();

return (

tracks.length > 0 &&

tracks.some(track => track.readyState === "live")

);

}

/**

* ==============================

* 监听麦克风Track

* ==============================

*/

bindTrackEvents() {

if (!this.stream) {

return;

}

this.stream

.getAudioTracks()

.forEach(track => {

track.onended =

this.handleTrackEnded;

});

}

handleTrackEnded = () => {

console.warn(

"VoiceRecorder 麦克风Track已经结束"

);

if (this.isRecording) {

this.stop();

}

};

/**

* ==============================

* 释放Stream

* ==============================

*/

releaseStream() {

if (!this.stream) {

return;

}

this.stream

.getTracks()

.forEach(track => {

track.onended = null;

track.stop();

});

this.stream = null;

}

/**

* ==============================

* 清除最大录音定时器

* ==============================

*/

clearMaxDurationTimer() {

if (this.maxDurationTimer) {

clearTimeout(

this.maxDurationTimer

);

this.maxDurationTimer = null;

}

}

/**

* ==============================

* 错误统一处理

* ==============================

*/

normalizeError(error) {

if (error instanceof Error) {

return error;

}

const message =

error?.message ||

"未知录音错误";

const normalized =

new Error(message);

if (error?.name) {

normalized.name = error.name;

}

return normalized;

}

/**

* ==============================

* 获取状态

* ==============================

*/

getState() {

if (this.destroyed) {

return "destroyed";

}

if (this.isRecording) {

return "recording";

}

if (this.isStopping) {

return "stopping";

}

return "idle";

}

/**

* ==============================

* 获取当前Stream

* ==============================

*/

getStream() {

return this.stream;

}

/**

* ==============================

* 销毁

* ==============================

*/

destroy() {

if (this.destroyed) {

return;

}

this.destroyed = true;

this.clearMaxDurationTimer();

// 停止Recorder

if (

this.mediaRecorder &&

this.mediaRecorder.state !== "inactive"

) {

try {

this.mediaRecorder.stop();

} catch (error) {

console.warn(

"VoiceRecorder stop失败:",

error

);

}

}

// 释放麦克风

this.releaseStream();

// 移除事件

this.el.removeEventListener(

"pointerdown",

this.handlePointerDown

);

this.el.removeEventListener(

"pointerup",

this.handlePointerUp

);

this.el.removeEventListener(

"pointercancel",

this.handlePointerCancel

);

this.el.removeEventListener(

"pointerleave",

this.handlePointerLeave

);

this.mediaRecorder = null;

this.chunks = \[\];

this.isRecording = false;

this.isStopping = false;

}

}

相关推荐
anyup1 小时前
【2026年8月】uView Pro 千星,还拿到了 GVP
前端·uni-app·github
必须会一定会1 小时前
用纯 HTML/JS 做一个 AI 需求澄清器:把模糊想法转换成可执行任务书
开发语言·前端·javascript·人工智能·html·ai编程
10mAh2 小时前
【Linux】error while loading shared libraries 怎么解决?——ldd、RPATH 与动态链接排错
java·linux·前端
深念Y2 小时前
stable-diffusion.cpp 的 FLUX.2 Klein 9B 分步
java·前端·数据库
小帅不太帅2 小时前
1.5M 参数的 OCR 模型,我把它跑进了浏览器
前端·javascript·ai编程
风骏时光牛马2 小时前
AI编程场景下故障根因分析与复盘总结
前端
Sterting2 小时前
Element-Plus 入门与整体布局
前端·vue.js·elementui
程序员黑豆9 小时前
Java 注释详解:单行、多行与文档注释的完整指南
java·前端·ai编程
剪刀石头布啊9 小时前
antd中可编辑表格中巧用useWatch实现联动高性能效果,以及定制延伸学习
前端