赞
踩
兼容多个浏览器下的前端录音功能,实现六大录音功能:
1、开始录音
2、暂停录音
3、继续录音
4、结束录音
5、播放录音
6、上传录音
初始状态:
开始录音:
结束录音:
录音流程 :
示例中的三个按钮其实包含了六个上述功能,点击开始时开始录音,可以暂停/结束录音,此操作后就可以播放播音/上传录音了噢~以下是对应六大录音功能示例代码,那大家会发现HZRecorder是啥呢? 其实 HZRecorder 是录音类,我们调用的都是该类里面的方法。
那大家肯定好奇,录音是通过怎样一种形式存在呢?其实用的就是浏览器的AudioContext对象,他旨在创建一个音频dom,有输入和输出。具体想了解这对象的,可以去mdn看看
- /**
- * 录音前准备 检查录音设备是否到位
- */
- this.readyRecording = async function() {
- let recorder // 表示录音类实例
- // 流模式下ready钩子 res 为录音类实例 或者 false
- await HZRecorder.ready().then(res => {
- recorder = res
- })
- return recorder
- }
- /**
- * 开始录音
- */
- this.startRecording = function() {
- recorder.start();
- }
- /**
- * 结束录音
- */
- this.stopRecording = function() {
- recorder.end();
- }
- /**
- * 播放录音
- */
- this.playRecording = function() {
- recorder.play(audio);
- }
- /**
- * 继续录音
- */
- this.resumeRecord = function() {
- recorder.again();
- }
- /**
- * 暂停录音
- */
- this.pauseRecord = function() {
- recorder.stop();
- }
- /**
- * 重新录音
- */
- this.reRecord = function() {
- this.startRecording()
- }
- /**
- * 上传录音
- */
- this.uploadRecord = function() {
- // 流模式下上传
- recorder.upload(url, succ, fail)
- }

大家看到这标题就好奇,啥叫流模式下的录音类呢?那还有其他模式吗?的确,我总结了下,是根据上传录音时的数据来区分的~我们常规情况下,上传录音都是流模式,也就是Content-Type为application/octem-stream,源码如下
- /**
- * 录音类(针对content-type为application/octem-stream 的使用)
- * @param {*} stream
- * @param {*} config
- */
- const HZRecorder = function (stream, config) {
- config = config || {};
- config.sampleBits = config.sampleBits || 8; //采样数位 8, 16
- config.sampleRate = config.sampleRate || (44100 / 6); //采样率(1/6 44100)
-
-
- //创建一个音频环境对象
- audioContext = window.AudioContext || window.webkitAudioContext;
- var context = new audioContext();
-
- //将声音输入这个对像
- var audioInput = context.createMediaStreamSource(stream);
-
- //设置音量节点
- var volume = context.createGain();
- audioInput.connect(volume);
-
- //创建缓存,用来缓存声音
- var bufferSize = 4096;
-
- // 创建声音的缓存节点,createScriptProcessor方法的
- // 第二个和第三个参数指的是输入和输出都是双声道。
- var recorder = context.createScriptProcessor(bufferSize, 2, 2);
-
- var audioData = {
- size: 0 //录音文件长度
- , buffer: [] //录音缓存
- , inputSampleRate: context.sampleRate //输入采样率
- , inputSampleBits: 16 //输入采样数位 8, 16
- , outputSampleRate: config.sampleRate //输出采样率
- , oututSampleBits: config.sampleBits //输出采样数位 8, 16
- , input: function (data) {
- this.buffer.push(new Float32Array(data));
- this.size += data.length;
- }
- , compress: function () { //合并压缩
- //合并
- var data = new Float32Array(this.size);
- var offset = 0;
- for (var i = 0; i < this.buffer.length; i++) {
- data.set(this.buffer[i], offset);
- offset += this.buffer[i].length;
- }
- //压缩
- var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
- var length = data.length / compression;
- var result = new Float32Array(length);
- var index = 0, j = 0;
- while (index < length) {
- result[index] = data[j];
- j += compression;
- index++;
- }
- return result;
- }
- , encodeWAV: function () {
- var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
- var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
- var bytes = this.compress();
- var dataLength = bytes.length * (sampleBits / 8);
- var buffer = new ArrayBuffer(44 + dataLength);
- var data = new DataView(buffer);
-
- var channelCount = 1;//单声道
- var offset = 0;
-
- var writeString = function (str) {
- for (var i = 0; i < str.length; i++) {
- data.setUint8(offset + i, str.charCodeAt(i));
- }
- };
-
- // 资源交换文件标识符
- writeString('RIFF'); offset += 4;
- // 下个地址开始到文件尾总字节数,即文件大小-8
- data.setUint32(offset, 36 + dataLength, true); offset += 4;
- // WAV文件标志
- writeString('WAVE'); offset += 4;
- // 波形格式标志
- writeString('fmt '); offset += 4;
- // 过滤字节,一般为 0x10 = 16
- data.setUint32(offset, 16, true); offset += 4;
- // 格式类别 (PCM形式采样数据)
- data.setUint16(offset, 1, true); offset += 2;
- // 通道数
- data.setUint16(offset, channelCount, true); offset += 2;
- // 采样率,每秒样本数,表示每个通道的播放速度
- data.setUint32(offset, sampleRate, true); offset += 4;
- // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
- data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
- // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
- data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
- // 每样本数据位数
- data.setUint16(offset, sampleBits, true); offset += 2;
- // 数据标识符
- writeString('data'); offset += 4;
- // 采样数据总数,即数据总大小-44
- data.setUint32(offset, dataLength, true); offset += 4;
- // 写入采样数据
- if (sampleBits === 8) {
- for (var i = 0; i < bytes.length; i++, offset++) {
- var s = Math.max(-1, Math.min(1, bytes[i]));
- var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
- val = parseInt(255 / (65535 / (val + 32768)));
- data.setInt8(offset, val, true);
- }
- } else {
- for (var i = 0; i < bytes.length; i++, offset += 2) {
- var s = Math.max(-1, Math.min(1, bytes[i]));
- data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
- }
- }
-
- return new Blob([data], { type: 'audio/wav' });
- }
- };
- //开始录音
- this.start = function () {
- audioInput.connect(recorder);
- recorder.connect(context.destination);
- };
-
- //停止
- this.stop = function () {
- recorder.disconnect();
- };
-
- // 结束
- this.end = function () {
- context.close();
- };
-
- // 继续
- this.again = function () {
- recorder.connect(context.destination);
- };
-
- //获取音频文件
- this.getBlob = function () {
- this.stop();
- return audioData.encodeWAV();
- };
-
- //回放
- this.play = function (audio) {
- audio.src = window.URL.createObjectURL(this.getBlob());
- };
-
- //上传
- this.upload = function (url, succ, fail) {
- const xhr = new XMLHttpRequest();
- xhr.overrideMimeType("application/octet-stream")
- // xhr.upload.addEventListener('progress', function (e) {
- // }, false);
- xhr.addEventListener('load', function (e) {
- succ(xhr.response)
- }, false);
- xhr.addEventListener('error', function (e) {
- fail(xhr.response);
- }, false);
- xhr.addEventListener('abort', function (e) {
- fail(xhr.response);
- }, false);
- xhr.open('POST', url);
- if(xhr.sendAsBinary){
- xhr.sendAsBinary(this.getBlob());
- }else{
- xhr.send(this.getBlob());
- }
- };
-
- //音频采集
- recorder.onaudioprocess = function (e) {
- audioData.input(e.inputBuffer.getChannelData(0));
- };
- }
-
- /**
- * 多浏览器兼容
- * @param {*} videoConfig 参数配置
- * @param {*} succ 成功回调
- * @param {*} fail 失败回调
- * @returns promise
- */
- HZRecorder.compatibleMedia = async function(videoConfig) {
- let streamPromise // 视频promise
- if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
- // 最新标准API
- streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
- } else if (navigator.webkitGetUserMedia){
- // webkit内核浏览器
- streamPromise = await navigator.webkitGetUserMedia(videoConfig)
- } else if (navigator.mozGetUserMedia){
- // Firefox浏览器
- streamPromise = await navagator.mozGetUserMedia(videoConfig)
- } else if (navigator.getUserMedia){
- // 旧版API
- streamPromise = await navigator.getUserMedia(videoConfig)
- }
- return streamPromise
- }
- /**
- * 是否支持录音
- * @returns 支持直接返回录音类实例 : 返回false
- */
- HZRecorder.ready = async function() {
- let instance // 录音类实例(ready ok) | false (ready no)
- await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
- instance = new HZRecorder(stream);
- }).catch(() => {
- instance = false
- })
- return instance
- }

和上述流模式的录音类有区别的是,表单模式下适用于上传录音时Content-Type为application/x-www-form-urlencoded噢~
- // --------------------------------------------------
- /**
- * 录音类(指定content-type为application/x-www-form-urlencoded使用)
- * @param {*} stream 流对象
- */
- const HZRecorderForm = function (stream) {
-
- //创建一个音频环境对象
- audioContext = window.AudioContext || window.webkitAudioContext;
- var ac = new audioContext();
- var chunks = [];
- var mediaRecorder
- var blobResult
- //开始录音
- this.start = function () {
- if (!mediaRecorder) {
- var origin = ac.createMediaStreamSource(stream)
- var dest = ac.createMediaStreamDestination();
- mediaRecorder = new MediaRecorder(dest.stream);
- mediaRecorder.ondataavailable = function(e) {
- chunks.push(e.data);
- }
- mediaRecorder.onstop = function(evt) {
- blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
-
- };
- origin.connect(dest);
- }
- mediaRecorder.start();
- };
- // 结束录音
- this.end = function () {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.requestData()
- mediaRecorder.stop();
- };
- // 暂停录音
- this.stop = function() {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.pause()
- }
- // 恢复录音
- this.again = function() {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.resume()
- }
- //上传
- this.upload = function (url, succ, err) {
- setTimeout(() => {
- var xhr = new XMLHttpRequest();
- xhr.open('POST', url);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
- xhr.send(blobResult);
- xhr.onload = e => {
- // 请求完成 && 外部状态码200 && 内部状态码1(这个内部状态码自定义)
- if (xhr.readyState === 4 && xhr.status === 200 && JSON.parse(xhr.response).status === 1) {
- succ && succ(xhr.response)
- } else {
- err && err(JSON.parse(xhr.response).message)
- }
- }
- })
- };
- }
-
- /**
- * 多浏览器兼容
- * @param {*} videoConfig 参数配置
- * @param {*} succ 成功回调
- * @param {*} fail 失败回调
- * @returns promise
- */
- HZRecorderForm.compatibleMedia = async function(videoConfig) {
- let streamPromise // 视频promise
- if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
- // 最新标准API
- streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
- } else if (navigator.webkitGetUserMedia){
- // webkit内核浏览器
- streamPromise = await navigator.webkitGetUserMedia(videoConfig)
- } else if (navigator.mozGetUserMedia){
- // Firefox浏览器
- streamPromise = await navagator.mozGetUserMedia(videoConfig)
- } else if (navigator.getUserMedia){
- // 旧版API
- streamPromise = await navigator.getUserMedia(videoConfig)
- }
- return streamPromise
- }
- /**
- * 是否支持录音
- * @returns 支持直接返回录音类实例 : 返回false
- */
- HZRecorderForm.ready = async function() {
- let instance // 录音类实例(ready ok) | false (ready no)
- await HZRecorderForm.compatibleMedia({ audio: true }).then(stream => {
- instance = new HZRecorderForm(stream);
- }).catch(() => {
- instance = false
- })
- return instance
- }

必须噢,你也可以自己实现这功能,HZRecorder.ready()方法返回的是promise对象,其值在当前有麦克风时候,返回的是录音类实例,你拿到此值就可以调用录音类的方法,无麦克风时候,返回的是false,表示当前不具备录音环境~
- /**
- * 录音前准备 检查录音设备是否到位
- */
- this.readyRecording = async function() {
- let recorder // 表示录音类实例
- // 流模式下ready钩子 res 为录音类实例 或者 false
- await HZRecorder.ready().then(res => {
- recorder = res
- })
- // 表单模式下ready钩子 res 为录音类实例 或者 false
- await HZRecorderForm.ready().then(res => {
- recorder = res
- })
- return recorder
- }
是的噢,火狐浏览器的navigator对象没有mediaDevices这个属性,所以这也是我为啥在录音类里要加入compatibleMedia方法,此方法就是用来兼容各个浏览器的噢~火狐就是用的navigator.mozGetUserMedia方法
- /**
- * 多浏览器兼容
- * @param {*} videoConfig 参数配置
- * @param {*} succ 成功回调
- * @param {*} fail 失败回调
- * @returns promise
- */
- HZRecorder.compatibleMedia = async function(videoConfig) {
- let streamPromise // 视频promise
- if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
- // 最新标准API
- streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
- } else if (navigator.webkitGetUserMedia){
- // webkit内核浏览器
- streamPromise = await navigator.webkitGetUserMedia(videoConfig)
- } else if (navigator.mozGetUserMedia){
- // Firefox浏览器
- streamPromise = await navagator.mozGetUserMedia(videoConfig)
- } else if (navigator.getUserMedia){
- // 旧版API
- streamPromise = await navigator.getUserMedia(videoConfig)
- }
- return streamPromise
- }

这个就有点涉及异步的知识了,在一个异步函数里,return是属于同步逻辑噢,promise.then属于异步,所以 return 会先于 .then执行的噢,这就和我们的想法不一致了,所以要await 阻塞代码,拿到instance值了,再返回
- /**
- * 是否支持录音
- * @returns 支持直接返回录音类实例 : 返回false
- */
- HZRecorder.ready = async function() {
- let instance // 录音类实例(ready ok) | false (ready no)
- await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
- instance = new HZRecorder(stream);
- }).catch(() => {
- instance = false
- })
- return instance
- }
不一定要使用原生xhr噢,你也可以根据你需求来修改成axios/fetch/ajax等~这个不影响整体代码的使用
其实在外层是上传接口的请求头区别,但在实际上,只是由于流模式下的写法,无法将音频转成mp3格式(默认为wav格式),当然网上也有小伙伴认为引入lame库来实现wav转换mp3的操作,当然可以啦~这不影响,只是对我来说,我是能不引入第三方库就不引入。
而表单模式实际上用的浏览器支持的另一个接口 MediaRecorder
而MediaRecorder是专门来做录制的,他想转换格式的话,就简单的多,在录制完触发onstop时,将可以将二进制数据转换成任意想要的格式,audio/mpeg就是mp3的格式~
- mediaRecorder.ondataavailable = function(e) {
- chunks.push(e.data);
- }
- mediaRecorder.onstop = function(evt) {
- blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
-
- };
因为
MediaRecorder 接口 有三个状态 inactive, recording, paused
这三个状态分别是设备闲置,设备使用,设备暂停,有点类似于window的未响应,当我们想要操作麦克风时,此时麦克风inactive了,那就无法响应我们的请求,所以当状态为inactive时,我们都return掉,使他不执行我们的方法。
- // 结束录音
- this.end = function () {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.requestData()
- mediaRecorder.stop();
- };
- // 暂停录音
- this.stop = function() {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.pause()
- }
- // 恢复录音
- this.again = function() {
- // 当录音类处于不活跃状态时,停止操作
- if (mediaRecorder.state === 'inactive') return
- mediaRecorder.resume()
- }

------有不懂的可以评论区聊聊噢~------
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。