React-Native實現(xiàn)仿微信發(fā)送語音

最近在做社交功能誓焦,需要收發(fā)語音双妨,所以就仿照微信做了一個語音錄制功能

使用的是react-native-audio

GitHub地址:https://github.com/jsierles/react-native-audio
配置按照GitHub上配置就可以,挺好配置的

iOS支持的編碼格式:lpcm, ima4, aac, MAC3, MAC6, ulaw, alaw, mp1, mp2, alac, amr
Android支持的編碼:aac, aac_eld, amr_nb, amr_wb, he_aac, vorbis

簡單說下我遇到的問題,android上錄制的在ios上不能播放最后發(fā)現(xiàn)錄制的語音設(shè)置參數(shù)少設(shè)置了
最后把錄制格式設(shè)定為如下android和ios問題完美解決

        AudioRecorder.prepareRecordingAtPath(audioPath, {
            SampleRate: 22050,
            Channels: 1,
            AudioQuality: 'Low',
            AudioEncoding: 'aac',
            OutputFormat: 'aac_adts',
        });

效果圖如下:


總體思路就是把下面的小方塊注冊為手勢模塊去監(jiān)聽用戶手勢的變化,然后在state里面設(shè)置一些參數(shù)根據(jù)手勢的變化給用戶反饋

import {AudioRecorder, AudioUtils} from 'react-native-audio';
/*
                this.audioPath
                注意這個是你錄音后文件的路徑+文件名仑嗅,
                可以使用react-native-audio下的AudioUtils路徑也可以使用其他路徑,
                如果名稱重復(fù)會覆蓋掉原有的錄音文件
*/
    this.audioPat = AudioUtils.DocumentDirectoryPath + '/test.aac', //路徑下的文件名
        this.state = {
            actionVisible: false,
            paused: false,
            recordingText: "",
            opacity: 'white',
            recordingColor: "transparent",
            text: I18n.t('message.Chat.Voice.holdToTalk'),
            currentTime: null,        //開始錄音到現(xiàn)在的持續(xù)時間
            recording: false,         //是否正在錄音
            stoppedRecording: false,  //是否停止了錄音
            finished: false,          //是否完成錄音
            hasPermission: undefined, //是否獲取權(quán)限
        }
  componentDidMount() {
    this.prepareRecordingPath(this.audioPath);
    //添加監(jiān)聽
    AudioRecorder.onProgress = (data) => {
      this.setState({
        currentTime: Math.floor(data.currentTime)
      }, () => {
        if (this.state.currentTime >= maxTime) {
          Alert.alert(I18n.t('message.Chat.Voice.speakTooLong'))
          this._cancel(false)
        }
      });
    };
    AudioRecorder.onFinished = (data) => {
      // Android callback comes in the form of a promise instead.
      if (Platform.OS === 'ios') {
        this._finishRecording(data.status === "OK", data.audioFileURL);
      }
    };
    //手勢
    this.Gesture = {
      onStartShouldSetResponder: (evt) => true,
      onMoveShouldSetResponder: (evt) => true,
      onResponderGrant: (evt) => {
        if (!this.state.hasPermission) {
          Alert.alert(I18n.t('message.Chat.Voice.jurisdiction'))
        }
        this.setState({
          opacity: "#c9c9c9",
          recordingText: I18n.t('message.Chat.Voice.fingerStroke'),
          text: I18n.t('message.Chat.Voice.releaseEnd'),
          icon: "ios-mic",
          recordingColor: 'transparent'
        }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
        this._record();
      },
      onResponderReject: (evt) => {
      },
      onResponderMove: (evt) => {
        if (evt.nativeEvent.pageY < this.recordPageY - UpperDistance) {
          if (this.state.recordingColor != 'red') {
            this.setRecordView(I18n.t('message.Chat.Voice.loosenFingers'), 'red', "ios-mic-off")
          }
        } else if (this.state.recordingColor != 'transparent') {
          this.setRecordView(I18n.t('message.Chat.Voice.fingerStroke'), 'transparent', "ios-mic")
        }
      },
      onResponderRelease: (evt) => {
        this.setState({
          opacity: "white",
          text: I18n.t('message.Chat.Voice.holdToTalk')
        });
        RecordView.hide();
        let canceled;
        if (evt.nativeEvent.locationY < 0 ||
          evt.nativeEvent.pageY < this.recordPageY) {
          canceled = true;
        } else {
          canceled = false;
        }
        this._cancel(canceled)
      },
      onResponderTerminationRequest: (evt) => true,
      onResponderTerminate: (evt) => {
      },
    }
  }
  setRecordView = (recordingText, recordingColor, icon) => {
    this.setState({
      recordingText: recordingText,
      recordingColor: recordingColor,
      icon: icon
    }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
  }
render() {
    const { opacity, text } = this.state
    return (
      <View style={styles.Box}>
        <View
          {...this.Gesture}        //注冊為手勢組件
          onLayout={this.handleLayout}
          ref={(record) => this.record = record}
          style={[styles.textBoxStyles, { backgroundColor: opacity }]}>
          <Text>{text}</Text>
        </View>
      </View>
    )
  }

上面彈出層的浮框?qū)崿F(xiàn)為

使用的是一個三方庫teaset
GitHub地址:https://github.com/rilyu/teaset

class RecordView {
  static key = null;
  static show(text, color, icon) {
    let showIcon;
    if (RecordView.key) RecordView.hide()
    if (color == 'red') {
      showIcon = (<Image source={ic_ch3x} style={styles.imageStyles} />)
    } else if (icon == 'short') {
      showIcon = (<Image source={SHORT4} style={styles.imageStyles} />)
    } else {
      showIcon = (
        <View>
          <Icon name={'ios-mic'} style={styles.IconStyles} />
          <Spinner size={24} type="ThreeBounce" color='white' />
        </View>
      );
    }

    RecordView.key = Toast.show({
      text: (
        <Text style={[styles.textStyles, { backgroundColor: color }]}>
          {text}
        </Text>
      ),
      icon: showIcon,
      position: 'center',
      duration: 1000000,
    });
  }
    static hide() {
        if (!RecordView.key) return;
        Toast.hide(RecordView.key);
        RecordView.key = null;
    }
}

我把代碼直接貼上吧直奋,沒單獨從項目中摘出來,就貼整個文件了

import React, { Component } from 'react';
import {
  Image, PermissionsAndroid, Alert,
  Platform, UIManager, findNodeHandle, DeviceEventEmitter
} from 'react-native';
import styles from './Styles';
import { Toast } from 'teaset';
import I18n from '../../../../I18n';
import { UpperDistance } from '../config';
import Spinner from "react-native-spinkit";
import { Text, Icon, View } from 'native-base'
import { AudioRecorder } from 'react-native-audio';
import Permissions from 'react-native-permissions';
import SHORT4 from '../../../../Images/C2CImg/SHORT4.png';
import ic_ch3x from '../../../../Images/C2CImg/ic_ch3x.png';
import MessageUtil from '../../MessageUtilModel/MessageUtil';
const maxTime = 45;  //最大時間
const minTime = 1; //最小時間
export default class Voice extends Component {
  constructor(props) {
    super(props)
    this.state = {
      paused: false,
      recordingText: "",
      opacity: 'white',
      recordingColor: "transparent",
      text: I18n.t('message.Chat.Voice.holdToTalk'),
      currentTime: null,        //開始錄音到現(xiàn)在的持續(xù)時間
      recording: false,         //是否正在錄音
      stoppedRecording: false,  //是否停止了錄音
      finished: false,          //是否完成錄音
      hasPermission: undefined, //是否獲取權(quán)限
    }
    // 語音存儲路徑
    const { userId } = this.props.chatFriend || {}
    this.audioPath = MessageUtil.getFilePath(userId, `${(new Date()).getTime()}.aac`)
  }
  componentDidMount() {
    this._checkPermission()
    this.prepareRecordingPath(this.audioPath);
    AudioRecorder.onProgress = (data) => {
      this.setState({
        currentTime: Math.floor(data.currentTime)
      }, () => {
        if (this.state.currentTime >= maxTime) {
          Alert.alert(I18n.t('message.Chat.Voice.speakTooLong'))
          this._cancel(false)
        }
      });
    };
    AudioRecorder.onFinished = (data) => {
      // Android callback comes in the form of a promise instead.
      if (Platform.OS === 'ios') {
        this._finishRecording(data.status === "OK", data.audioFileURL);
      }
    };
    this.Gesture = {
      onStartShouldSetResponder: (evt) => true,
      onMoveShouldSetResponder: (evt) => true,
      onResponderGrant: (evt) => {
        if (!this.state.hasPermission) {
          Alert.alert(I18n.t('message.Chat.Voice.jurisdiction'))
        }
        this.setState({
          opacity: "#c9c9c9",
          recordingText: I18n.t('message.Chat.Voice.fingerStroke'),
          text: I18n.t('message.Chat.Voice.releaseEnd'),
          icon: "ios-mic",
          recordingColor: 'transparent'
        }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
        this._record();
      },
      onResponderMove: (evt) => {
        if (evt.nativeEvent.pageY < this.recordPageY - UpperDistance) {
          if (this.state.recordingColor != 'red') {
            this.setRecordView(I18n.t('message.Chat.Voice.loosenFingers'), 'red', "ios-mic-off")
          }
        } else if (this.state.recordingColor != 'transparent') {
          this.setRecordView(I18n.t('message.Chat.Voice.fingerStroke'), 'transparent', "ios-mic")
        }
      },
      onResponderRelease: (evt) => {
        this.setState({
          opacity: "white",
          text: I18n.t('message.Chat.Voice.holdToTalk')
        });
        RecordView.hide();
        let canceled;
        if (evt.nativeEvent.locationY < 0 ||
          evt.nativeEvent.pageY < this.recordPageY) {
          canceled = true;
        } else {
          canceled = false;
        }
        this._cancel(canceled)
      },
      onResponderTerminationRequest: (evt) => true
    }
    //chatChange type 1前臺 0后臺 2中間
    this.ShowLocation = DeviceEventEmitter.addListener('chatChange', (type) => {
      this._cancel(false)
    })
  }
  componentWillUnmount() {
    this.ShowLocation.remove()
    AudioRecorder.removeListeners()
    this.timer && clearTimeout(this.timer);
  }
  prepareRecordingPath = (audioPath) => {
    AudioRecorder.prepareRecordingAtPath(audioPath, {
      SampleRate: 22050,
      Channels: 1,
      AudioQuality: 'Low',
      AudioEncoding: 'aac',
      OutputFormat: 'aac_adts',
    });
  }
  _checkPermission = async () => {
    const rationale = {
      'title': I18n.t('message.Chat.Voice.tips'),
      'message': I18n.t('message.Chat.Voice.tipsMessage')
    };
    let askForGrant = false
    if (Platform.OS === 'ios') {
      Permissions.check('microphone', { type: 'always' }).then(res => {
        if (res == 'authorized') {
          this.setState({ hasPermission: true })
        } else {
          Permissions.request('microphone', { type: 'always' }).then(response => {
            if (response == 'denied') {
              askForGrant = true
            } else if (response == 'authorized') {
              this.setState({ hasPermission: true });
            }
          });
        }
      });
    } else {
      const status = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, rationale)
      if (status !== "granted") {
        askForGrant = true
      } else {
        this.setState({ hasPermission: true });
      }
    }
    if (askForGrant) {
      Alert.alert(
        'Can we access your microphone and Speech Recognition?',
        'We need access so you can record your voice',
        [
          {
            text: 'Later',
            onPress: () => console.log('Permission denied'),
            style: 'cancel',
          },
          {
            text: 'Open Settings',
            onPress: Permissions.openSettings
          },
        ],
      );
    }
  }
  setRecordView = (recordingText, recordingColor, icon) => {
    this.setState({
      recordingText: recordingText,
      recordingColor: recordingColor,
      icon: icon
    }, _ => RecordView.show(recordingText, recordingColor, icon));
  }

  _cancel = (canceled) => {
    let filePath = this._stop();
    if (canceled) return;
    if (this.state.currentTime < minTime) {
      this.setRecordView(I18n.t('message.Chat.Voice.speakTooShort'), 'transparent', "short")
      this.timer = setTimeout(() => { RecordView.hide() }, 300)
      return;
    }
    this.setState({ currentTime: null })
    let voice = {
      audioPath: this.audioPath,
      currentTime: this.state.currentTime
    }
    setTimeout(() => { this.props.SendVoice(voice) }, 500)
  }
  _pause = async () => {
    if (!this.state.recording) return;
    try {
      const filePath = await AudioRecorder.pauseRecording();
      this.setState({ paused: true });
    } catch (error) {
    }
  }

  _resume = async () => {
    if (!this.state.paused) return;
    try {
      await AudioRecorder.resumeRecording();
      this.setState({ paused: false });
    } catch (error) {
    }
  }

  _stop = async () => {
    if (!this.state.recording) return;

    this.setState({ stoppedRecording: true, recording: false, paused: false });

    try {
      const filePath = await AudioRecorder.stopRecording();

      if (Platform.OS === 'android') {
        this._finishRecording(true, filePath);
      }
      return filePath;
    } catch (error) {
    }
  }
  _finishRecording = (didSucceed, filePath) => {
    this.setState({ finished: didSucceed });
  }
  _record = async () => {
    const { recording, hasPermission, stoppedRecording } = this.state
    const { userId } = this.props.chatFriend || {}
    if (recording) return;
    if (!hasPermission) return;
    if (stoppedRecording) {
      this.audioPath = MessageUtil.getFilePath(userId, `${(new Date()).getTime()}.aac`)
      this.prepareRecordingPath(this.audioPath);
    }
    this.setState({
      recording: true,
      paused: false
    });

    try {
      const filePath = await AudioRecorder.startRecording();
    } catch (error) {
    }
  }
  handleLayout = () => {
    const handle = findNodeHandle(this.record);
    UIManager.measure(handle, (x, y, w, h, px, py) => {
      // this._ownMeasurements = { x, y, w, h, px, py };
      this.recordPageX = px;
      this.recordPageY = py;
    });
  }
  render() {
    const { opacity, text } = this.state
    return (
      <View style={styles.Box}>
        <View
          {...this.Gesture}
          onLayout={this.handleLayout}
          ref={(record) => this.record = record}
          style={[styles.textBoxStyles, { backgroundColor: opacity }]}>
          <Text>{text}</Text>
        </View>
      </View>
    )
  }
}
class RecordView {
  static key = null;
  static show(text, color, icon) {
    let showIcon;
    if (RecordView.key) RecordView.hide()
    if (color == 'red') {
      showIcon = (<Image source={ic_ch3x} style={styles.imageStyles} />)
    } else if (icon == 'short') {
      showIcon = (<Image source={SHORT4} style={styles.imageStyles} />)
    } else {
      showIcon = (
        <>
          <Icon name={'ios-mic'} style={styles.IconStyles} />
          <Spinner size={24} type="ThreeBounce" color='white' />
        </>
      );
    }

    RecordView.key = Toast.show({
      text: (
        <Text style={[styles.textStyles, { backgroundColor: color }]}>
          {text}
        </Text>
      ),
      icon: showIcon,
      position: 'center',
      duration: 1000000,
    });
  }
  static hide() {
    if (!RecordView.key) return;
    Toast.hide(RecordView.key);
    RecordView.key = null;
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末施禾,一起剝皮案震驚了整個濱河市帮碰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拾积,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丰涉,死亡現(xiàn)場離奇詭異拓巧,居然都是意外死亡,警方通過查閱死者的電腦和手機一死,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門肛度,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人投慈,你說我怎么就攤上這事承耿。” “怎么了伪煤?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵加袋,是天一觀的道長。 經(jīng)常有香客問我抱既,道長职烧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任防泵,我火速辦了婚禮蚀之,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捷泞。我一直安慰自己足删,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布锁右。 她就那樣靜靜地躺著失受,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咏瑟。 梳的紋絲不亂的頭發(fā)上贱纠,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音响蕴,去河邊找鬼谆焊。 笑死,一個胖子當(dāng)著我的面吹牛浦夷,可吹牛的內(nèi)容都是我干的辖试。 我是一名探鬼主播辜王,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼罐孝!你這毒婦竟也來了呐馆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤莲兢,失蹤者是張志新(化名)和其女友劉穎汹来,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體改艇,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡收班,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谒兄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摔桦。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖承疲,靈堂內(nèi)的尸體忽然破棺而出邻耕,到底是詐尸還是另有隱情,我是刑警寧澤燕鸽,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布兄世,位于F島的核電站,受9級特大地震影響啊研,放射性物質(zhì)發(fā)生泄漏碘饼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一悲伶、第九天 我趴在偏房一處隱蔽的房頂上張望艾恼。 院中可真熱鬧,春花似錦麸锉、人聲如沸钠绍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柳爽。三九已至,卻和暖如春碱屁,著一層夾襖步出監(jiān)牢的瞬間磷脯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工娩脾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赵誓,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像俩功,于是被迫代替她去往敵國和親幻枉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容