当前位置:   article > 正文

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

js 录音

1、主题描述

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

1、开始录音

2、暂停录音

3、继续录音

4、结束录音

5、播放录音

6、上传录音

2、示例功能

初始状态:

开始录音:

结束录音:

录音流程 :

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

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

AudioContext

  1. /**
  2. * 录音前准备 检查录音设备是否到位
  3. */
  4. this.readyRecording = async function() {
  5. let recorder // 表示录音类实例
  6. // 流模式下ready钩子 res 为录音类实例 或者 false
  7. await HZRecorder.ready().then(res => {
  8. recorder = res
  9. })
  10. return recorder
  11. }
  12. /**
  13. * 开始录音
  14. */
  15. this.startRecording = function() {
  16. recorder.start();
  17. }
  18. /**
  19. * 结束录音
  20. */
  21. this.stopRecording = function() {
  22. recorder.end();
  23. }
  24. /**
  25. * 播放录音
  26. */
  27. this.playRecording = function() {
  28. recorder.play(audio);
  29. }
  30. /**
  31. * 继续录音
  32. */
  33. this.resumeRecord = function() {
  34. recorder.again();
  35. }
  36. /**
  37. * 暂停录音
  38. */
  39. this.pauseRecord = function() {
  40. recorder.stop();
  41. }
  42. /**
  43. * 重新录音
  44. */
  45. this.reRecord = function() {
  46. this.startRecording()
  47. }
  48. /**
  49. * 上传录音
  50. */
  51. this.uploadRecord = function() {
  52. // 流模式下上传
  53. recorder.upload(url, succ, fail)
  54. }

3、流模式下的录音类

大家看到这标题就好奇,啥叫流模式下的录音类呢?那还有其他模式吗?的确,我总结了下,是根据上传录音时的数据来区分的~我们常规情况下,上传录音都是流模式,也就是Content-Type为application/octem-stream,源码如下

  1. /**
  2. * 录音类(针对content-type为application/octem-stream 的使用)
  3. * @param {*} stream
  4. * @param {*} config
  5. */
  6. const HZRecorder = function (stream, config) {
  7. config = config || {};
  8. config.sampleBits = config.sampleBits || 8; //采样数位 8, 16
  9. config.sampleRate = config.sampleRate || (44100 / 6); //采样率(1/6 44100)
  10. //创建一个音频环境对象
  11. audioContext = window.AudioContext || window.webkitAudioContext;
  12. var context = new audioContext();
  13. //将声音输入这个对像
  14. var audioInput = context.createMediaStreamSource(stream);
  15. //设置音量节点
  16. var volume = context.createGain();
  17. audioInput.connect(volume);
  18. //创建缓存,用来缓存声音
  19. var bufferSize = 4096;
  20. // 创建声音的缓存节点,createScriptProcessor方法的
  21. // 第二个和第三个参数指的是输入和输出都是双声道。
  22. var recorder = context.createScriptProcessor(bufferSize, 2, 2);
  23. var audioData = {
  24. size: 0 //录音文件长度
  25. , buffer: [] //录音缓存
  26. , inputSampleRate: context.sampleRate //输入采样率
  27. , inputSampleBits: 16 //输入采样数位 8, 16
  28. , outputSampleRate: config.sampleRate //输出采样率
  29. , oututSampleBits: config.sampleBits //输出采样数位 8, 16
  30. , input: function (data) {
  31. this.buffer.push(new Float32Array(data));
  32. this.size += data.length;
  33. }
  34. , compress: function () { //合并压缩
  35. //合并
  36. var data = new Float32Array(this.size);
  37. var offset = 0;
  38. for (var i = 0; i < this.buffer.length; i++) {
  39. data.set(this.buffer[i], offset);
  40. offset += this.buffer[i].length;
  41. }
  42. //压缩
  43. var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
  44. var length = data.length / compression;
  45. var result = new Float32Array(length);
  46. var index = 0, j = 0;
  47. while (index < length) {
  48. result[index] = data[j];
  49. j += compression;
  50. index++;
  51. }
  52. return result;
  53. }
  54. , encodeWAV: function () {
  55. var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
  56. var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
  57. var bytes = this.compress();
  58. var dataLength = bytes.length * (sampleBits / 8);
  59. var buffer = new ArrayBuffer(44 + dataLength);
  60. var data = new DataView(buffer);
  61. var channelCount = 1;//单声道
  62. var offset = 0;
  63. var writeString = function (str) {
  64. for (var i = 0; i < str.length; i++) {
  65. data.setUint8(offset + i, str.charCodeAt(i));
  66. }
  67. };
  68. // 资源交换文件标识符
  69. writeString('RIFF'); offset += 4;
  70. // 下个地址开始到文件尾总字节数,即文件大小-8
  71. data.setUint32(offset, 36 + dataLength, true); offset += 4;
  72. // WAV文件标志
  73. writeString('WAVE'); offset += 4;
  74. // 波形格式标志
  75. writeString('fmt '); offset += 4;
  76. // 过滤字节,一般为 0x10 = 16
  77. data.setUint32(offset, 16, true); offset += 4;
  78. // 格式类别 (PCM形式采样数据)
  79. data.setUint16(offset, 1, true); offset += 2;
  80. // 通道数
  81. data.setUint16(offset, channelCount, true); offset += 2;
  82. // 采样率,每秒样本数,表示每个通道的播放速度
  83. data.setUint32(offset, sampleRate, true); offset += 4;
  84. // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
  85. data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
  86. // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
  87. data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
  88. // 每样本数据位数
  89. data.setUint16(offset, sampleBits, true); offset += 2;
  90. // 数据标识符
  91. writeString('data'); offset += 4;
  92. // 采样数据总数,即数据总大小-44
  93. data.setUint32(offset, dataLength, true); offset += 4;
  94. // 写入采样数据
  95. if (sampleBits === 8) {
  96. for (var i = 0; i < bytes.length; i++, offset++) {
  97. var s = Math.max(-1, Math.min(1, bytes[i]));
  98. var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
  99. val = parseInt(255 / (65535 / (val + 32768)));
  100. data.setInt8(offset, val, true);
  101. }
  102. } else {
  103. for (var i = 0; i < bytes.length; i++, offset += 2) {
  104. var s = Math.max(-1, Math.min(1, bytes[i]));
  105. data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
  106. }
  107. }
  108. return new Blob([data], { type: 'audio/wav' });
  109. }
  110. };
  111. //开始录音
  112. this.start = function () {
  113. audioInput.connect(recorder);
  114. recorder.connect(context.destination);
  115. };
  116. //停止
  117. this.stop = function () {
  118. recorder.disconnect();
  119. };
  120. // 结束
  121. this.end = function () {
  122. context.close();
  123. };
  124. // 继续
  125. this.again = function () {
  126. recorder.connect(context.destination);
  127. };
  128. //获取音频文件
  129. this.getBlob = function () {
  130. this.stop();
  131. return audioData.encodeWAV();
  132. };
  133. //回放
  134. this.play = function (audio) {
  135. audio.src = window.URL.createObjectURL(this.getBlob());
  136. };
  137. //上传
  138. this.upload = function (url, succ, fail) {
  139. const xhr = new XMLHttpRequest();
  140. xhr.overrideMimeType("application/octet-stream")
  141. // xhr.upload.addEventListener('progress', function (e) {
  142. // }, false);
  143. xhr.addEventListener('load', function (e) {
  144. succ(xhr.response)
  145. }, false);
  146. xhr.addEventListener('error', function (e) {
  147. fail(xhr.response);
  148. }, false);
  149. xhr.addEventListener('abort', function (e) {
  150. fail(xhr.response);
  151. }, false);
  152. xhr.open('POST', url);
  153. if(xhr.sendAsBinary){
  154. xhr.sendAsBinary(this.getBlob());
  155. }else{
  156. xhr.send(this.getBlob());
  157. }
  158. };
  159. //音频采集
  160. recorder.onaudioprocess = function (e) {
  161. audioData.input(e.inputBuffer.getChannelData(0));
  162. };
  163. }
  164. /**
  165. * 多浏览器兼容
  166. * @param {*} videoConfig 参数配置
  167. * @param {*} succ 成功回调
  168. * @param {*} fail 失败回调
  169. * @returns promise
  170. */
  171. HZRecorder.compatibleMedia = async function(videoConfig) {
  172. let streamPromise // 视频promise
  173. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
  174. // 最新标准API
  175. streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
  176. } else if (navigator.webkitGetUserMedia){
  177. // webkit内核浏览器
  178. streamPromise = await navigator.webkitGetUserMedia(videoConfig)
  179. } else if (navigator.mozGetUserMedia){
  180. // Firefox浏览器
  181. streamPromise = await navagator.mozGetUserMedia(videoConfig)
  182. } else if (navigator.getUserMedia){
  183. // 旧版API
  184. streamPromise = await navigator.getUserMedia(videoConfig)
  185. }
  186. return streamPromise
  187. }
  188. /**
  189. * 是否支持录音
  190. * @returns 支持直接返回录音类实例 : 返回false
  191. */
  192. HZRecorder.ready = async function() {
  193. let instance // 录音类实例(ready ok) | false (ready no)
  194. await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
  195. instance = new HZRecorder(stream);
  196. }).catch(() => {
  197. instance = false
  198. })
  199. return instance
  200. }

4、表单模式下的录音类

和上述流模式的录音类有区别的是,表单模式下适用于上传录音时Content-Type为application/x-www-form-urlencoded噢~

  1. // --------------------------------------------------
  2. /**
  3. * 录音类(指定content-type为application/x-www-form-urlencoded使用)
  4. * @param {*} stream 流对象
  5. */
  6. const HZRecorderForm = function (stream) {
  7. //创建一个音频环境对象
  8. audioContext = window.AudioContext || window.webkitAudioContext;
  9. var ac = new audioContext();
  10. var chunks = [];
  11. var mediaRecorder
  12. var blobResult
  13. //开始录音
  14. this.start = function () {
  15. if (!mediaRecorder) {
  16. var origin = ac.createMediaStreamSource(stream)
  17. var dest = ac.createMediaStreamDestination();
  18. mediaRecorder = new MediaRecorder(dest.stream);
  19. mediaRecorder.ondataavailable = function(e) {
  20. chunks.push(e.data);
  21. }
  22. mediaRecorder.onstop = function(evt) {
  23. blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
  24. };
  25. origin.connect(dest);
  26. }
  27. mediaRecorder.start();
  28. };
  29. // 结束录音
  30. this.end = function () {
  31. // 当录音类处于不活跃状态时,停止操作
  32. if (mediaRecorder.state === 'inactive') return
  33. mediaRecorder.requestData()
  34. mediaRecorder.stop();
  35. };
  36. // 暂停录音
  37. this.stop = function() {
  38. // 当录音类处于不活跃状态时,停止操作
  39. if (mediaRecorder.state === 'inactive') return
  40. mediaRecorder.pause()
  41. }
  42. // 恢复录音
  43. this.again = function() {
  44. // 当录音类处于不活跃状态时,停止操作
  45. if (mediaRecorder.state === 'inactive') return
  46. mediaRecorder.resume()
  47. }
  48. //上传
  49. this.upload = function (url, succ, err) {
  50. setTimeout(() => {
  51. var xhr = new XMLHttpRequest();
  52. xhr.open('POST', url);
  53. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
  54. xhr.send(blobResult);
  55. xhr.onload = e => {
  56. // 请求完成 && 外部状态码200 && 内部状态码1(这个内部状态码自定义)
  57. if (xhr.readyState === 4 && xhr.status === 200 && JSON.parse(xhr.response).status === 1) {
  58. succ && succ(xhr.response)
  59. } else {
  60. err && err(JSON.parse(xhr.response).message)
  61. }
  62. }
  63. })
  64. };
  65. }
  66. /**
  67. * 多浏览器兼容
  68. * @param {*} videoConfig 参数配置
  69. * @param {*} succ 成功回调
  70. * @param {*} fail 失败回调
  71. * @returns promise
  72. */
  73. HZRecorderForm.compatibleMedia = async function(videoConfig) {
  74. let streamPromise // 视频promise
  75. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
  76. // 最新标准API
  77. streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
  78. } else if (navigator.webkitGetUserMedia){
  79. // webkit内核浏览器
  80. streamPromise = await navigator.webkitGetUserMedia(videoConfig)
  81. } else if (navigator.mozGetUserMedia){
  82. // Firefox浏览器
  83. streamPromise = await navagator.mozGetUserMedia(videoConfig)
  84. } else if (navigator.getUserMedia){
  85. // 旧版API
  86. streamPromise = await navigator.getUserMedia(videoConfig)
  87. }
  88. return streamPromise
  89. }
  90. /**
  91. * 是否支持录音
  92. * @returns 支持直接返回录音类实例 : 返回false
  93. */
  94. HZRecorderForm.ready = async function() {
  95. let instance // 录音类实例(ready ok) | false (ready no)
  96. await HZRecorderForm.compatibleMedia({ audio: true }).then(stream => {
  97. instance = new HZRecorderForm(stream);
  98. }).catch(() => {
  99. instance = false
  100. })
  101. return instance
  102. }

5、疑难解答

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

必须噢,你也可以自己实现这功能,HZRecorder.ready()方法返回的是promise对象,其值在当前有麦克风时候,返回的是录音类实例,你拿到此值就可以调用录音类的方法,无麦克风时候,返回的是false,表示当前不具备录音环境~

  1. /**
  2. * 录音前准备 检查录音设备是否到位
  3. */
  4. this.readyRecording = async function() {
  5. let recorder // 表示录音类实例
  6. // 流模式下ready钩子 res 为录音类实例 或者 false
  7. await HZRecorder.ready().then(res => {
  8. recorder = res
  9. })
  10. // 表单模式下ready钩子 res 为录音类实例 或者 false
  11. await HZRecorderForm.ready().then(res => {
  12. recorder = res
  13. })
  14. return recorder
  15. }

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

是的噢,火狐浏览器的navigator对象没有mediaDevices这个属性,所以这也是我为啥在录音类里要加入compatibleMedia方法,此方法就是用来兼容各个浏览器的噢~火狐就是用的navigator.mozGetUserMedia方法

  1. /**
  2. * 多浏览器兼容
  3. * @param {*} videoConfig 参数配置
  4. * @param {*} succ 成功回调
  5. * @param {*} fail 失败回调
  6. * @returns promise
  7. */
  8. HZRecorder.compatibleMedia = async function(videoConfig) {
  9. let streamPromise // 视频promise
  10. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
  11. // 最新标准API
  12. streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
  13. } else if (navigator.webkitGetUserMedia){
  14. // webkit内核浏览器
  15. streamPromise = await navigator.webkitGetUserMedia(videoConfig)
  16. } else if (navigator.mozGetUserMedia){
  17. // Firefox浏览器
  18. streamPromise = await navagator.mozGetUserMedia(videoConfig)
  19. } else if (navigator.getUserMedia){
  20. // 旧版API
  21. streamPromise = await navigator.getUserMedia(videoConfig)
  22. }
  23. return streamPromise
  24. }

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

这个就有点涉及异步的知识了,在一个异步函数里,return是属于同步逻辑噢,promise.then属于异步,所以 return 会先于 .then执行的噢,这就和我们的想法不一致了,所以要await 阻塞代码,拿到instance值了,再返回

  1. /**
  2. * 是否支持录音
  3. * @returns 支持直接返回录音类实例 : 返回false
  4. */
  5. HZRecorder.ready = async function() {
  6. let instance // 录音类实例(ready ok) | false (ready no)
  7. await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
  8. instance = new HZRecorder(stream);
  9. }).catch(() => {
  10. instance = false
  11. })
  12. return instance
  13. }

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

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

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

其实在外层是上传接口的请求头区别,但在实际上,只是由于流模式下的写法,无法将音频转成mp3格式(默认为wav格式),当然网上也有小伙伴认为引入lame库来实现wav转换mp3的操作,当然可以啦~这不影响,只是对我来说,我是能不引入第三方库就不引入。

而表单模式实际上用的浏览器支持的另一个接口 MediaRecorder

而MediaRecorder是专门来做录制的,他想转换格式的话,就简单的多,在录制完触发onstop时,将可以将二进制数据转换成任意想要的格式,audio/mpeg就是mp3的格式~

  1. mediaRecorder.ondataavailable = function(e) {
  2. chunks.push(e.data);
  3. }
  4. mediaRecorder.onstop = function(evt) {
  5. blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
  6. };

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

因为 MediaRecorder 接口 有三个状态 inactive, recording, paused

这三个状态分别是设备闲置,设备使用,设备暂停,有点类似于window的未响应,当我们想要操作麦克风时,此时麦克风inactive了,那就无法响应我们的请求,所以当状态为inactive时,我们都return掉,使他不执行我们的方法。

  1. // 结束录音
  2. this.end = function () {
  3. // 当录音类处于不活跃状态时,停止操作
  4. if (mediaRecorder.state === 'inactive') return
  5. mediaRecorder.requestData()
  6. mediaRecorder.stop();
  7. };
  8. // 暂停录音
  9. this.stop = function() {
  10. // 当录音类处于不活跃状态时,停止操作
  11. if (mediaRecorder.state === 'inactive') return
  12. mediaRecorder.pause()
  13. }
  14. // 恢复录音
  15. this.again = function() {
  16. // 当录音类处于不活跃状态时,停止操作
  17. if (mediaRecorder.state === 'inactive') return
  18. mediaRecorder.resume()
  19. }

------有不懂的可以评论区聊聊噢~------

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/571412
推荐阅读
相关标签
  

闽ICP备14008679号