h5实现录音功能(navigator.mediaDevices)

封装组件(soundRecording.vue):

<template>
  <view class="recorder"> </view>
</template>

<script>
export default {
  data() {
    return {
      isUserMedia: false,
      stream: null,
      audio: null,
      recorder: null,
      chunks: []
    };
  },
  mounted() {
    /**
     * 	error 事件的返回状态
     * 	100: 请在HTTPS环境中使用
     * 	101: 浏览器不支持
     *  201: 用户拒绝授权
     *  500: 未知错误
     * */
    if (origin.indexOf('https') === -1) {
      this.$emit('error', '100');
      uni.showModal({
        title: '提示',
        content: '请在https环境中使用录音功能,确认返回上一个页面',
        showCancel: false,
        success: (res) => {
          if (res.confirm) {
            uni.navigateBack();
          }
        }
      });
    }
    if (!navigator.mediaDevices || !window.MediaRecorder) {
      this.$emit('error', '101');
      uni.showModal({
        title: '提示',
        content: '当前浏览器不支持录音功能,确认返回上一个页面',
        showCancel: false,
        success: (res) => {
          if (res.confirm) {
            uni.navigateBack();
          }
        }
      });
    }
    this.getRecorderManager();
  },
  methods: {
    getRecorderManager() {
      this.audio = document.createElement('audio');
      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then((stream) => {
          this.isUserMedia = true;
          stream.getTracks().forEach((track) => {
            track.stop();
          });
        })
        .catch((err) => {
          this.onErrorHandler(err);
        });
    },
    start() {
      if (!this.isUserMedia) {
        uni.showModal({
          title: '提示',
          content: '当前设备不支持,确认返回上一个页面',
          showCancel: false,
          success: (res) => {
            if (res.confirm) {
              uni.navigateBack();
            }
          }
        });
      }
      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then((stream) => {
          this.stream = stream;
          this.recorder = new MediaRecorder(stream);
          this.recorder.ondataavailable = this.getRecordingData;
          this.recorder.onstop = this.saveRecordingData;
          this.recorder.start();
        })
        .catch((err) => {
          this.onErrorHandler(err);
        });
    },
    stop() {
      if (this.recorder) {
        this.recorder.stop();
        this.stream.getTracks().forEach((track) => {
          track.stop();
        });
      }
    },
    getRecordingData(e) {
      this.chunks.push(e.data);
    },
    saveRecordingData() {
      const blob = new Blob(this.chunks, { type: 'audio/mpeg' }),
        localUrl = URL.createObjectURL(blob);
      this.chunks = [];
      let lock = true;
      const temporaryAudio = document.createElement('audio');
      temporaryAudio.src = localUrl;
      temporaryAudio.muted = true;
      temporaryAudio.load();
      temporaryAudio.play();
      temporaryAudio.addEventListener('timeupdate', (e) => {
        if (!Number.isFinite(temporaryAudio.duration)) {
          temporaryAudio.currentTime = Number.MAX_SAFE_INTEGER;
          temporaryAudio.currentTime = 0;
        } else {
          document.body.append(temporaryAudio);
          document.body.removeChild(temporaryAudio);
          if (lock) {
            lock = false;
            const recorder = {
              data: blob,
              duration: temporaryAudio.duration,
              localUrl: localUrl
            };
            this.$emit('success', recorder);
          }
        }
      });
    },
    onErrorHandler(err) {
      console.log(err);
      if (err.name === 'NotAllowedError') {
        this.$emit('error', '201');
        uni.showModal({
          title: '提示',
          content: '用户拒绝了当前浏览器的访问请求,确认返回上一个页面',
          showCancel: false,
          success: (res) => {
            if (res.confirm) {
              uni.navigateBack();
            }
          }
        });
      }

      if (err.name === 'NotReadableError') {
        this.$emit('error', '101');
        uni.showModal({
          title: '提示',
          content: '当前浏览器不支持,确认返回上一个页面',
          showCancel: false,
          success: (res) => {
            if (res.confirm) {
              uni.navigateBack();
            }
          }
        });
      }

      this.$emit('error', '500');
      uni.showModal({
        title: '提示',
        content: '调用失败,确认返回上一个页面',
        showCancel: false,
        success: (res) => {
          if (res.confirm) {
            uni.navigateBack();
          }
        }
      });
    }
  },
  destroyed() {
    this.stop();
  }
};
</script>

基础使用组件:

<template>
  <view>
    <view class="audio" v-if="recorder">
      <audio :src="recorder.localUrl" name="本地录音" controls="true"></audio>
      <br />
      <button type="primary" @click="handlerSave">保存录音</button>
    </view>
    <h3 v-else>点击下方按钮录音</h3>
    <div class="container" v-if="status">
      <div class="wave0"></div>
      <div class="wave1"></div>
    </div>
    <view @click="handlerOnCahnger" class="statusBox" :class="{ active: status }">
      <view class="status"></view>
    </view>
    <sound-recording ref="recorder" @success="handlerSuccess" @error="handlerError"></sound-recording>
  </view>
</template>

<script>
import SoundRecording from '@/components/soundRecording/soundRecording.vue';
export default {
  components: {
    SoundRecording
  },
  data() {
    return {
      status: false,
      recorder: null
    };
  },
  methods: {
    handlerSave() {
      uni.downloadFile({
        url: this.recorder.localUrl, //仅为示例,并非真实的资源
        success: (res) => {
          if (res.statusCode === 200) {
            var oA = document.createElement('a');
            oA.download = ''; // 设置下载的文件名,默认是'下载'
            oA.href = res.tempFilePath; //临时路径再保存到本地
            document.body.appendChild(oA);
            oA.click();
            oA.remove(); // 下载之后把创建的元素删除
          }
        },
        fail: (err) => {
          uni.showToast({
            title: `下载失败${err}`
          });
        }
      });
    },
    handlerOnCahnger() {
      if (this.status) {
        this.$refs.recorder.stop();
      } else {
        this.$refs.recorder.start();
      }
      this.status = !this.status;
    },
    handlerSuccess(res) {
      console.log(res);
      this.recorder = res;
    },
    handlerError(code) {
      switch (code) {
        case '101':
          uni.showModal({
            content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。'
          });
          break;
        case '201':
          uni.showModal({
            content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
          });
          break;
        default:
          uni.showModal({
            content: '未知错误,请刷新页面重试'
          });
          break;
      }
    }
  }
};
</script>

<style lang="scss" scoped>
.audio {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20rpx;
}

h3 {
  text-align: center;
  padding: 20rpx;
}

.statusBox {
  z-index: 11;
  width: 120rpx;
  height: 120rpx;
  background-color: aliceblue;
  border-radius: 50%;
  box-shadow: 5rpx 5rpx 10rpx rgba(000, 000, 000, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed;
  left: 50%;
  bottom: 120rpx;
  transform: translateX(-50%);

  .status {
    width: 60rpx;
    height: 60rpx;
    background-color: aliceblue;
    border-radius: 50%;
    border: 10rpx solid #5c5c66;
  }

  &.active {
    background-color: rgba(118, 218, 255, 0.45);

    .status {
      background-color: rgba(118, 218, 255, 0.45);
      border: 10rpx solid rgba(255, 255, 255, 0.75);
    }
  }
}

.container {
  z-index: 10;
  position: fixed;
  bottom: -200rpx;
  padding: 0;
  border: 0;
  width: 750rpx;
  height: 750rpx;
  background-color: rgb(118, 218, 255);
}

.wave0,
.wave1,
.wave2 {
  position: absolute;
  width: 750rpx * 2;
  height: 750rpx * 2;
  margin-top: -150%;
  margin-left: -50%;
  background-color: rgba(255, 255, 255, 0.4);
  border-radius: 45%;
  animation: spin 15s linear -0s infinite;
  z-index: 1;
  /*            border: 1px solid;*/
}

.wave1 {
  margin-top: -152%;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 47%;
  animation: spin 30s linear -15s infinite;
  z-index: 2;
}

@keyframes spin {
  0% {
    transform: translate(-0%, -0%) rotate(0deg) scale(1);
  }

  25% {
    transform: translate(-1%, -1%) rotate(90deg) scale(1);
  }

  50% {
    transform: translate(-0%, -2%) rotate(180deg) scale(1);
  }

  75% {
    transform: translate(1%, -1%) rotate(270deg) scale(1);
  }

  100% {
    transform: translate(-0%, -0%) rotate(360deg) scale(1);
  }
}
</style>

效果:

按照这个功能写了一个类似微信语音聊天功能:

<template>
  <view>
    <view class="record">
      <block v-for="(item, index) in records" :key="index">
        <view class="item">
          <view class="content" v-if="item.type === 'text'">{{ item.content }}</view>
          <view class="content" v-else-if="item.type === 'img'">
            <image :src="item.content" mode="widthFix" @click="openImg(item.content)"></image>
          </view>
          <view class="content" :class="{ active: playItemIndex === index }" v-else-if="item.type === 'recorder'" @click="playRecorder(index)">
            <view class="recorder">
              <view class="time" :style="{ marginRight: changerWidth(item.content.duration) }"> {{ item.content.duration | integer }}" </view>
              <view class="icon">
                <text class="on1">(</text>
                <text class="on2">(</text>
                <text class="on3">(</text>
              </view>
            </view>
          </view>
          <!-- <image class="hander" src="/static/imitate_weChat/logo.jpg" /> -->
        </view>
      </block>
    </view>

    <view class="toolBox">
      <view class="recorder" :class="{ active: isUseRecorder }" @touchstart.prevent="startRecorder" @touchend.prevent="endRecorder">
        {{ isUseRecorder ? '松开 结束' : '按住 说话' }}
      </view>
      <!-- <view class="camrea" @click="camera">55</view> -->
    </view>
    <view class="toolBg"></view>

    <sound-recording ref="recorderRef" @success="handlerSuccess" @error="handlerError"></sound-recording>
  </view>
</template>

<script>
import SoundRecording from '@/components/soundRecording/soundRecording.vue';
export default {
  components: { SoundRecording },
  data() {
    return {
      audio: null,
      records: [
        {
          type: 'text',
          content: 'Hello World!'
        },
        {
          type: 'text',
          content: 'Hello!'
        }
      ],

      isUseRecorder: false,
      playItemIndex: -1,
      currentAudio: ''
    };
  },
  mounted() {
    this.audio = document.createElement('audio');
    this.audio.addEventListener('ended', () => {
      this.playItemIndex = -1;
      this.currentAudio = '';
    });
  },
  methods: {
    openImg(img) {
      uni.previewImage({
        urls: [img]
      });
    },
    startRecorder() {
      this.$refs.recorderRef.start();
      this.isUseRecorder = true;
    },
    endRecorder() {
      this.$refs.recorderRef.stop();
      this.isUseRecorder = false;
    },
    playRecorder(index) {
      this.playItemIndex = index;
      this.currentAudio = this.records[index].content.localUrl;
      this.audio.src = this.currentAudio;
      this.audio.play();
    },
    handlerSuccess(res) {
      if (res.duration < 1)
        return uni.showToast({
          title: '语言时间小于1秒',
          icon: 'error'
        });

      this.records.push({
        type: 'recorder',
        content: res
      });
    },

    changerWidth(v) {
      let a = 40;
      let b = v * 20;
      if (b > 450) b = 450;
      return a + b + 'rpx';
    },

    handlerError(code) {
      switch (code) {
        case '101':
          uni.showModal({
            content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。'
          });
          break;
        case '201':
          uni.showModal({
            content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
          });
          break;
        default:
          uni.showModal({
            content: '未知错误,请刷新页面重试'
          });
          break;
      }
    },

    camera() {
      uni.showToast({
        title: '录像模块开发中',
        icon: 'none'
      });
    }
  },
  filters: {
    integer(v) {
      return Math.ceil(v);
    }
  }
};
</script>

<style lang="scss" scoped>
page {
  background-color: #f1eded;
}

.record {
  padding: 20rpx;
  font-size: 28rpx;

  .item {
    display: flex;
    justify-content: flex-end;
    padding: 10rpx;
    margin-bottom: 15rpx;

    .content {
      margin-right: 20rpx;
      position: relative;
      background-color: rgba(107, 197, 107, 0.85);
      padding: 20rpx 30rpx;
      border-radius: 10rpx;

      &::before {
        position: absolute;
        right: -8px;
        top: 8px;
        content: '';
        display: inline-block;
        width: 0;
        height: 0;
        border: 4px solid transparent;
        border-left-color: rgba(107, 197, 107, 0.85);
      }

      image {
        max-width: 400rpx;
      }

      .recorder {
        display: flex;
        align-items: center;

        .time {
          margin-right: 40rpx;
        }

        .icon {
          font-weight: bold;
          color: #fff;
          font-size: 32rpx;
          display: flex;
          align-items: center;

          text {
            display: block;
          }

          & text:nth-of-type(1) {
            transform: scale(1.2);
          }

          & text:nth-of-type(2) {
            transform: scale(0.8);
          }

          & text:nth-of-type(3) {
            transform: scale(0.6);
          }
        }
      }

      &.active {
        .recorder {
          .icon {
            animation: play 1.5s ease-in-out infinite backwards;
          }
        }
      }
    }

    .hander {
      width: 80rpx;
      height: 80rpx;
      border-radius: 50%;
    }
  }
}

.toolBg {
  height: 140rpx;
}

.toolBox {
  border-top: 1px solid #ccc;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  padding: 0rpx 30rpx;
  height: 140rpx;
  background-color: rgba(255, 255, 255, 0.45);

  .recorder {
    width: 550rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #fff;
    padding: 17rpx;
    border-radius: 10rpx;
    font-size: 28rpx;
    box-shadow: 2rpx 3rpx 10rpx rgba(0, 0, 0, 0.2);
    position: relative;

    &.active {
      background-color: #95a5a6;

      &::before {
        position: absolute;
        left: 50%;
        transform: translate(-50%);
        top: -3px;
        content: '';
        width: 0;
        height: 3px;
        background-color: #7bed9f;
        animation: loading-data 1.25s ease-in-out infinite backwards;
      }
    }
  }

  @keyframes loading-data {
    0% {
      width: 0;
    }

    100% {
      width: 100%;
    }
  }

  @keyframes play {
    0% {
      color: #fff;
    }

    50% {
      color: #c3c3c3;
    }

    100% {
      columns: #fff;
    }
  }

  .camrea {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}
</style>

效果:

                              

 注意:需要调用录音功能的域名必须是https,否则调用失败!

 

posted @ 2023-04-11 16:06  zaijinyang  阅读(2214)  评论(0编辑  收藏  举报