音频h5录制开发

class VoiceRecorder {

constructor(options = {}) {

this.targetElement = options.targetElement;

this.maxDuration = options.maxDuration ?? 60000;

this.cancelDistance = options.cancelDistance ?? 80;

this.onStart = options.onStart;

this.onStop = options.onStop;

this.onCancel = options.onCancel;

this.onComplete = options.onComplete;

this.onError = options.onError;

this.stream = null;

this.recorder = null;

this.chunks = \[\];

this.isRecording = false;

this.isCancel = false;

this.startTime = 0;

this.timer = null;

this.startY = 0;

const check = this.check();

if (!check.respstate) {

throw new Error(check.result.message);

}

this.bind();

}

/**

* 参数及浏览器能力检查

*

* @returns {{

* respstate: boolean,

* result: Object

* }}

*/

check() {

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

return {

respstate: false,

result: {

code: "INVALID_PARAM",

message: "targetElement必须是HTMLElement"

}

};

}

if (!window.isSecureContext) {

return {

respstate: false,

result: {

code: "UNSAFE_CONTEXT",

message: "当前环境不支持麦克风,请使用HTTPS访问"

}

};

}

if (

!navigator.mediaDevices ||

!navigator.mediaDevices.getUserMedia

) {

return {

respstate: false,

result: {

code: "NOT_SUPPORTED",

message: "当前浏览器不支持getUserMedia"

}

};

}

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

return {

respstate: false,

result: {

code: "NOT_SUPPORTED",

message: "当前浏览器不支持MediaRecorder"

}

};

}

const mimeType = this.getMimeType();

if (!mimeType) {

return {

respstate: false,

result: {

code: "MIME_NOT_SUPPORTED",

message: "当前浏览器没有支持的音频录制格式"

}

};

}

return {

respstate: true,

result: {

mimeType

}

};

}

/**

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

*/

getMimeType() {

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 (e) {

// 某些浏览器检测MIME时可能直接抛异常

}

}

return "";

}

/**

* 绑定事件

*/

bind() {

this.targetElement.addEventListener(

"pointerdown",

this.start

);

this.targetElement.addEventListener(

"pointermove",

this.move

);

this.targetElement.addEventListener(

"pointerup",

this.stop

);

this.targetElement.addEventListener(

"pointercancel",

this.cancel

);

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

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

}

/**

* 开始录音

*/

start = async (event) => {

event.preventDefault();

if (this.isRecording) {

return;

}

this.startY = event.clientY;

this.isCancel = false;

try {

if (!this.stream) {

this.stream =

await navigator.mediaDevices.getUserMedia({

audio: true,

video: false

});

}

const mimeType = this.getMimeType();

this.chunks = \[\];

this.recorder = new MediaRecorder(

this.stream,

mimeType

? { mimeType }

: undefined

);

this.recorder.ondataavailable = (event) => {

if (event.data && event.data.size > 0) {

this.chunks.push(event.data);

}

};

this.recorder.onstop = () => {

this.complete();

};

this.recorder.onerror = (event) => {

this.isRecording = false;

this.clearTimer();

this.onError?.(

event.error ||

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

);

};

this.recorder.start();

this.isRecording = true;

this.startTime = Date.now();

// 最大录音时间

this.timer = setTimeout(() => {

if (this.isRecording) {

// 超时属于正常结束

this.stop();

}

}, this.maxDuration);

this.onStart?.();

} catch (error) {

this.isRecording = false;

this.clearTimer();

this.onError?.(error);

}

};

/**

* 手指/鼠标移动

*

* 向上滑超过cancelDistance,取消录音

*/

move = (event) => {

if (!this.isRecording) {

return;

}

const distance = this.startY - event.clientY;

if (distance >= this.cancelDistance) {

this.cancel();

}

};

/**

* 正常停止录音

*/

stop = (event) => {

event?.preventDefault();

if (!this.isRecording || !this.recorder) {

return;

}

if (this.isCancel) {

return;

}

this.isRecording = false;

this.clearTimer();

if (this.recorder.state !== "inactive") {

this.recorder.stop();

}

this.onStop?.();

};

/**

* 取消录音

*/

cancel = () => {

if (!this.isRecording || !this.recorder) {

return;

}

this.isCancel = true;

this.isRecording = false;

this.clearTimer();

if (this.recorder.state !== "inactive") {

this.recorder.stop();

}

// 告诉业务:用户取消了录音

this.onCancel?.();

};

/**

* 录音完成

*/

complete() {

if (this.isCancel) {

this.chunks = \[\];

this.isCancel = false;

return;

}

if (!this.chunks.length) {

this.onError?.(

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

);

return;

}

const duration = Date.now() - this.startTime;

const type =

this.recorder?.mimeType ||

this.chunks0?.type ||

"audio/webm";

const blob = new Blob(this.chunks, {

type

});

this.chunks = \[\];

this.onComplete?.({

blob,

type: blob.type,

size: blob.size,

duration

});

}

/**

* 清除超时定时器

*/

clearTimer() {

if (this.timer) {

clearTimeout(this.timer);

this.timer = null;

}

}

/**

* 销毁

*/

destroy() {

this.clearTimer();

if (

this.recorder &&

this.recorder.state !== "inactive"

) {

try {

this.recorder.stop();

} catch (e) {

// ignore

}

}

if (this.stream) {

this.stream.getTracks().forEach(track => {

track.stop();

});

}

this.targetElement.removeEventListener(

"pointerdown",

this.start

);

this.targetElement.removeEventListener(

"pointermove",

this.move

);

this.targetElement.removeEventListener(

"pointerup",

this.stop

);

this.targetElement.removeEventListener(

"pointercancel",

this.cancel

);

this.stream = null;

this.recorder = null;

this.chunks = \[\];

this.isRecording = false;

this.isCancel = false;

}

}

相关推荐
小码哥_常3 分钟前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮2 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
她说可以呀4 小时前
Vue插槽
前端·javascript·vue.js
qq_448011164 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan4 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
2601_958352905 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
志尊宝5 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native
FRing036 小时前
JavaScript入门到精通三十二(深拷贝、浅拷贝、递归函数)
开发语言·前端·javascript
excel6 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
外收内放7 小时前
Python基础语法练习题(73)
java·前端·python