音频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;

}

}

相关推荐
大家的林语冰13 分钟前
👍 超越 ESLint,Oxc 优先采用 TypeScript 7,Rust 和 Go 梦幻联动!
前端·javascript·typescript
樊小肆1 小时前
# 你还在等DeepSeek官方 agent Harness‌? 来试试 DeepSeeker-Code吧
前端·人工智能·后端
樊小肆1 小时前
2568 万 token 才花 2 块 2:聊聊 DeepSeeker-Code 怎么吃满上下文缓存
前端·人工智能·后端
JarvanMo2 小时前
Flutter 3.47: material/cupertino终于解耦了
前端
郭邯2 小时前
一次「字符洁癖」引发的思考:我用 AI 写了个全角半角转换工具
前端
想要成为糕糕手2 小时前
🚀 在浏览器里跑 DeepSeek-R1?WebGPU 端侧推理实战(五)—— 中断、重置、缓存与流式生成
前端·react.js·llm
GuWenyue2 小时前
后端接口又双叒没写好?3 步搭建前端 Mock,从此告别"傻等后端"
前端·mocha
海兰2 小时前
【数据采集】开源的 Web 数据采集与处理Firecrawl(一)
前端·开源
vivo互联网技术2 小时前
从一键检测到 AI 修复:我们如何把无障碍检查做进研发流程
前端·人工智能
liuxiaocheng2 小时前
聊聊 Vercel AI SDK 的流式协议:前后端到底是怎么"边想边说"的
前端·后端