有趣且重要的JS知识合集(18)浏览器实现前端录音功能

这篇具有很好参考价值的文章主要介绍了有趣且重要的JS知识合集(18)浏览器实现前端录音功能。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、主题描述

兼容多个浏览器下的前端录音功能,实现六大录音功能:

1、开始录音

2、暂停录音

3、继续录音

4、结束录音

5、播放录音

6、上传录音

2、示例功能

初始状态:

js 录音,js有趣知识,前端,javascript

开始录音:

js 录音,js有趣知识,前端,javascript

结束录音:

js 录音,js有趣知识,前端,javascript

录音流程 :

示例中的三个按钮其实包含了六个上述功能,点击开始时开始录音,可以暂停/结束录音,此操作后就可以播放播音/上传录音了噢~以下是对应六大录音功能示例代码,那大家会发现HZRecorder是啥呢? 其实 HZRecorder 是录音类,我们调用的都是该类里面的方法。

那大家肯定好奇,录音是通过怎样一种形式存在呢?其实用的就是浏览器的AudioContext对象,他旨在创建一个音频dom,有输入和输出。具体想了解这对象的,可以去mdn看看

AudioContext

    /**
     * 录音前准备 检查录音设备是否到位
     */
    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)
    }

3、流模式下的录音类

大家看到这标题就好奇,啥叫流模式下的录音类呢?那还有其他模式吗?的确,我总结了下,是根据上传录音时的数据来区分的~我们常规情况下,上传录音都是流模式,也就是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
}

4、表单模式下的录音类

和上述流模式的录音类有区别的是,表单模式下适用于上传录音时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
}

5、疑难解答

1、在录音开始前都必须调用readyRecording方法吗?

必须噢,你也可以自己实现这功能,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
    }

2、火狐浏览器提示 navigator.mediaDevices is undefined,找不到?

是的噢,火狐浏览器的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
}

3、录音类的ready方法,为啥要使用async/await呢?

这个就有点涉及异步的知识了,在一个异步函数里,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
}

4、录音类为啥要使用 XMLHttpRequest 来触发接口呢?

不一定要使用原生xhr噢,你也可以根据你需求来修改成axios/fetch/ajax等~这个不影响整体代码的使用

5、流模式和表单模式的录音类本质上有啥区别?

其实在外层是上传接口的请求头区别,但在实际上,只是由于流模式下的写法,无法将音频转成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' });
  
        };

6、表单模式下的录音类为啥要判断inactive状态呢?

因为 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()
    }

------有不懂的可以评论区聊聊噢~------文章来源地址https://www.toymoban.com/news/detail-764582.html

到了这里,关于有趣且重要的JS知识合集(18)浏览器实现前端录音功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 开源语言模型的历史和重要性;Edge浏览器将推出Bing AI重写文本功能

    🦉 AI新闻 🚀 微软即将推出桌面版Microsoft Edge浏览器的Bing AI重写文本功能 摘要 :微软最近在桌面版Microsoft Edge浏览器中引入了一个新功能,允许用户使用Bing AI重写文本。用户可以选择不同的语气、格式和长度,然后通过重写按钮来生成Bing AI的回应。目前该功能正在向部分内

    2024年02月14日
    浏览(35)
  • js获取元素到可视区的距离/浏览器窗口滚动距离/元素距离浏览器顶部距离

    1. js获取元素距离可视区的各种距离 2. js获取浏览器窗口滚动距离 3. js获取元素实际距离页面距离(包括滚动距离) (1).如果父辈元素中有定位的元素,那么就返回距离当前元素最近的定位元素边缘的距离。 (2).如果父辈元素中没有定位元素,那么就返回相对于body边缘距

    2024年02月12日
    浏览(30)
  • 运行 Node.js 与浏览器 JavaScript

    浏览器和 Node.js 都使用 JavaScript 软件语言 - 但字面上的 运行时环境 是不同的。 Node.js(又名服务器端 JavaScript)与客户端 JavaScript 有许多相似之处。它也有很多差异。 尽管两者都使用 JavaScript 作为软件语言,但我们可以重点关注一些关键差异,这些差异使两者之间的软件开发

    2024年02月09日
    浏览(36)
  • js控制浏览器前进、后退、页面跳转

    在JavaScript中,你可以使用  window  对象的  history  对象来控制浏览器的历史记录。以下是一些常用的方法: 前进和后退 : window.history.forward() : 前进到历史记录中的下一个页面。 window.history.back() : 返回历史记录中的上一个页面。 window.history.go(n) : 跳转到历史记录中的指定页

    2024年01月20日
    浏览(31)
  • 最新JS判断是否是360浏览器方法

    总所周知,360浏览器UA信息和谷歌浏览器完全一致,之前的诸多奇葩招数基本也都修复了 目前测试可用的监测方案如下: 360浏览器修这玩意每次都挺快,不知道能用多久,2023年6月目前可用 原理就是检测dll文件,这个文件360浏览器 正常版本 和 极速版本 都没有 是根据网上代

    2024年02月15日
    浏览(30)
  • 【安全策略】前端 JS 安全对抗&浏览器调试方法

    1.1 什么是接口加密 如今这个时代,数据已经变得越来越重要,网页和APP是主流的数据载体。而如果获取数据的接口没有设置任何的保护措施,那么数据的安全性将面临极大的威胁。不仅可能造成数据的轻易窃取和篡改,还可能导致一些重要功能的接口被恶意调用,引发DDoS、

    2024年01月21日
    浏览(37)
  • js判断是否处于微信浏览器内

    当我们开发网页应用或移动应用时,经常需要根据用户当前的环境做一些特殊处理。在这篇文章中,我们将探讨如何使用 JavaScript 来判断当前页面是否在微信浏览器内打开。 微信是一款非常流行的社交媒体应用,拥有庞大的用户群体。为了提供更好的用户体验,我们可能需要

    2024年02月08日
    浏览(31)
  • JS一些常用判断(包括判断是否是苹果(ios)/安卓(Android)、是否是Safari浏览器、检测浏览器语言等等)

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 提示:这里可以添加本文要记录的大概内容: 参考链接 JS判断客户端是否是iOS或者Android:http://caibaojian.com/browser-ios-or-android.html

    2024年02月04日
    浏览(57)
  • 浏览器js打开本地exe程序(超详细、可用)

    目的::打开游览器链接,弹出打开本地exe操作 新建txt文件输入以下代码,可自行配置路径,路径必须为\\\\,且最后的 不可省略。 保存txt文件,并修改为xxx.reg文件。最后双击运行 执行HTML代码即可实现目的   双击66666666666666666666

    2024年02月11日
    浏览(43)
  • JS监听浏览器关闭、刷新及切换标签页触发事件

    蛮简单的东西,知道就会,不知道就不会,没什么逻辑可言。简单记录一下,只为加深点儿印象。 visibilitychange visibilitychange可以监听到浏览器的切换标签页。  直接上代码:  beforeunload  beforeunload可以监听到页面的关闭,页签切换不会触发 关闭浏览器窗口的时候触发 通过点

    2024年02月08日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包