移動端H5錄音組件開發(react版本)


基於AudioContextmediaDevices實現的原生js的錄音功能

// recorder.js,這個是在網上找的,具體地址不記得了,這個存在一個問題就是,他分段之后會把audioData清空,導致最后結束的時候,audioData是一個空值,如果需要把整段的錄音轉化成一個音頻文件,不考慮分片的話,可以把onaudioprocess里面的sendData注釋掉,沒錯,我就是這樣搞的,只需要一個完整的音頻,如果需要分段傳送,就把注釋打開,然后作出對應的處理
const Recorder = function (stream, callback) {
  const sampleBits = 16; //輸出采樣數位 8, 16
  const sampleRate = 8000; //輸出采樣率
  const context = new AudioContext();
  const audioInput = context.createMediaStreamSource(stream);
  const recorder = context.createScriptProcessor(4096, 1, 1);
  const audioData = {
    size: 0, //錄音文件長度
    buffer: [], //錄音緩存
    inputSampleRate: 48000, //輸入采樣率
    inputSampleBits: 16, //輸入采樣數位 8, 16
    outputSampleRate: sampleRate, //輸出采樣數位
    oututSampleBits: sampleBits, //輸出采樣率
    clear: function () {
      this.buffer = [];
      this.size = 0;
    },
    input: function (data) {
      this.buffer.push(new Float32Array(data));
      this.size += data.length;
    },
    compress: function () { //合並壓縮
      //合並
      const data = new Float32Array(this.size);
      let offset = 0;
      for (let i = 0; i < this.buffer.length; i++) {
        data.set(this.buffer[i], offset);
        offset += this.buffer[i].length;
      }
      //壓縮
      const compression = parseInt(this.inputSampleRate / this.outputSampleRate);
      const length = data.length / compression;
      const result = new Float32Array(length);
      let index = 0,
        j = 0;
      while (index < length) {
        result[index] = data[j];
        j += compression;
        index++;
      }
      return result;
    },
    encodePCM: function () { //這里不對采集到的數據進行其他格式處理,如有需要均交給服務器端處理。
      const sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
      const sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
      const bytes = this.compress();
      const dataLength = bytes.length * (sampleBits / 8);
      const buffer = new ArrayBuffer(dataLength);
      const data = new DataView(buffer);
      let offset = 0;
      for (let i = 0; i < bytes.length; i++, offset += 2) {
        const 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/pcm' });
    }
  };

  const sendData = function () { //對以獲取的數據進行處理(分包)
    const reader = new FileReader();
    reader.onload = e => {
      const outbuffer = e.target.result;
      // callback && callback(outbuffer);
      const arr = new Int8Array(outbuffer);
      if (arr.length > 0) {
        let tmparr = new Int8Array(1024);
        let j = 0;
        for (let i = 0; i < arr.byteLength; i++) {
          tmparr[j++] = arr[i];
          if (((i + 1) % 1024) == 0) {
            callback && callback(tmparr);
            if (arr.byteLength - i - 1 >= 1024) {
              tmparr = new Int8Array(1024);
            } else {
              tmparr = new Int8Array(arr.byteLength - i - 1);
            }
            j = 0;
          }
          if ((i + 1 == arr.byteLength) && ((i + 1) % 1024) != 0) {
            callback && callback(tmparr);
          }
        }
      }
    };
    reader.readAsArrayBuffer(audioData.encodePCM());
    audioData.clear();//每次發送完成則清理掉舊數據
  };

  this.start = function () {
    audioInput.connect(recorder);
    recorder.connect(context.destination);
  }

  this.stop = function () {
    recorder.disconnect();
  }

  this.getBlob = function () {
    return audioData.encodePCM();
  }

  this.clear = function () {
    audioData.clear();
  }

  recorder.onaudioprocess = function (e) {
    const inputBuffer = e.inputBuffer.getChannelData(0);
    audioData.input(inputBuffer);
    // sendData();
  }
}

export default Recorder;
/**
 * 錄音組件
 */
// RecordItem.js
import React, { Component } from 'react';
import { Icon } from 'antd';
import { Toast } from 'antd-mobile';

import Recorder from './Recorder';

import './RecordItem.less';

class RecordItem extends Component {

  state = {
    isRecording: false, // 是否正在錄音
  }

  timer = null; // 判斷長按的定時器

  handleTouchStart = () => {
    this.timer = setTimeout(() => {
      this.recorder.start();
      this.setState({
        isRecording: true
      });
    }, 300);
  }

  handleTouchEnd = () => {
    if (this.timer) {
      clearTimeout(this.timer);
    }
    this.recorder.stop();
    this.setState({
      isRecording: false
    }, () => {
      const { onEnd } = this.props;
      onEnd && onEnd(this.recorder.getBlob());
    });
  }

  // 處理錄音的回調
  handleMsg = (data) => {
    const { onProgress } = this.props;
    onProgress && onProgress(data);
  }

  componentDidMount() {
    const constraints = { audio: true };
    navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
      this.recorder = new Recorder(stream, this.handleMsg);
    }, err => {
      switch (err.message || err.name) {
        case 'PERMISSION_DENIED':
        case 'PermissionDeniedError':
          Toast.info('用戶拒絕提供信息。');
          break;
        case 'NOT_SUPPORTED_ERROR':
        case 'NotSupportedError':
          Toast.info('瀏覽器不支持硬件設備。');
          break;
        case 'MANDATORY_UNSATISFIED_ERROR':
        case 'MandatoryUnsatisfiedError':
          Toast.info('無法發現指定的硬件設備。');
          break;
        default:
          Toast.info('無法打開麥克風。異常信息:' + (err.code || err.name));
          break;
      }
    });
    // this.recorder = new Recorder({
    //   callback: this.handleMsg
    // });
  }

  render() {
    const { isRecording } = this.state;
    return (
      <div
        className={`RecordItem ${isRecording ? 'recording' : ''}`}
        onTouchStart={this.handleTouchStart}
        onTouchEnd={this.handleTouchEnd}
      >
        <Icon type="audio" />
      </div>
    );
  }
}

export default RecordItem;
// RecordItem.less
.RecordItem {
  color: #333333;

  &.recording {
    color: #10C0DC;
  }

  .anticon {
    font-size: 1.5rem;
  }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM