Vue實現懸浮框自由移動+錄音功能的示例代碼

效果如下

主要功能

1.一個漂浮的球,在全屏幕中自由移動遇到邊邊角角自動改變方向 ,自帶動畫效果

2.錄音功能,可以錄制用戶的聲音,可以下載為任意格式的音頻文件到本地,也可以通過二進制流發給後端

由於後端要聲音文件格式wav或者pcm,采樣率16000,所以我改瞭配置文件,稍後我會介紹在哪裡改,改什麼什麼樣都是可以的。

註:代碼我已經封裝成組件瞭,下方的代碼可以直接稍作修改後拿去用,需要修改的地方我以截圖的形式貼出來瞭。

實現

1.封裝第一個漂浮組件FloatBall.vue

<template>
  <!--懸浮小廣告樣式的提示信息-->
  <div
    id="thediv"
    ref="thediv"
    style="position: absolute; z-index: 111; left: 0; top: 0"
    @mouseover="clearFdAd"
    @mouseout="starFdAd"
  >
    <div
      style="
        overflow: hidden;
        cursor: pointer;
        text-align: right;
        font-size: 0.0625rem;
        color: #999999;
      "
    >
      <div @click="demo" style="position: relative">
        <!-- 錄音組件 -->
        <div v-show="isAudio">
          <Audio style="position: absolute; top: 10%; right: 20%"></Audio>
        </div>
        <div class="loader">
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
          <span></span>
        </div>
        <!-- <img src="@/assets/common/loginlogo.png" alt="" srcset="" /> -->
      </div>
    </div>
    <!-- <a
      href="http://xxxxxx" target="_blank""
    ><img src="../../assets/images/tips.png" width="320" border="0" /></a> -->
  </div>
</template>
<script>
 
var interval
export default {
  data () {
    return {
      isAudio: false,
      xPos: 0,
      yPos: 0,
      xin: true,
      yin: true,
      step: 1,
      delay: 18,
      height: 0,
      Hoffset: 0,
      Woffset: 0,
      yon: 0,
      xon: 0,
      pause: true,
      thedivShow: true,
    }
  },
 
  mounted () {
    interval = setInterval(this.changePos, this.delay)
  },
 
  methods: {
    demo () {
      this.isAudio = !this.isAudio
    },
    changePos () {
      let width = document.documentElement.clientWidth
      let height = document.documentElement.clientHeight
      this.Hoffset = this.$refs.thediv.clientHeight//獲取元素高度
      this.Woffset = this.$refs.thediv.offsetWidth
 
      // 滾動部分跟隨屏幕滾動
      // this.$refs.thediv.style.left = (this.xPos + document.body.scrollLeft + document.documentElement.scrollLeft) + "px";
      // this.$refs.thediv.style.top = (this.yPos + document.body.scrollTop + document.documentElement.scrollTop) + "px";
 
      // 滾動部分不隨屏幕滾動
      this.$refs.thediv.style.left =
        this.xPos + document.body.scrollLeft - 400 + "px"
      this.$refs.thediv.style.top = this.yPos + document.body.scrollTop + "px"
 
      if (this.yon) {
        this.yPos = this.yPos + this.step
      } else {
        this.yPos = this.yPos - this.step
      }
      if (this.yPos < 0) {
        this.yon = 1
        this.yPos = 0
      }
      if (this.yPos >= height - this.Hoffset) {
        this.yon = 0
        this.yPos = height - this.Hoffset
      }
 
      if (this.xon) {
        this.xPos = this.xPos + this.step
      } else {
        this.xPos = this.xPos - this.step
      }
      if (this.xPos < 0) {
        this.xon = 1
        this.xPos = 0
      }
      if (this.xPos >= width - this.Woffset) {
        this.xon = 0
        this.xPos = width - this.Woffset
      }
    },
    clearFdAd () {
      clearInterval(interval)
    },
    starFdAd () {
      interval = setInterval(this.changePos, this.delay)
    },
  },
};
</script>
 
<style lang="scss" scoped>
#thediv {
  z-index: 100;
  position: absolute;
  top: 0.224rem;
  left: 0.0104rem;
  height: 0.9583rem;
  width: 1.4583rem;
  overflow: hidden;
  img {
    width: 100%;
    height: 100%;
  }
}
// 以下是css圖標
.loader {
  width: 100px;
  height: 100px;
  padding: 30px;
  font-size: 10px;
  background-color: #91cc75;
  border-radius: 50%;
  border: 8px solid #20a088;
  display: flex;
  align-items: center;
  justify-content: space-between;
  animation: loader-animate 1.5s infinite ease-in-out;
}
 
@keyframes loader-animate {
  45%,
  55% {
    transform: scale(1);
  }
}
 
.loader > span {
  width: 5px;
  height: 50%;
  background-color: #fff;
  transform: scaleY(0.05) translateX(-5px);
  animation: span-animate 1.5s infinite ease-in-out;
  animation-delay: calc(var(--n) * 0.05s);
}
 
@keyframes span-animate {
  0%,
  100% {
    transform: scaleY(0.05) translateX(-5px);
  }
  15% {
    transform: scaleY(1.2) translateX(10px);
  }
  90%,
  100% {
    background-color: hotpink;
  }
}
 
.loader > span:nth-child(1) {
  --n: 1;
}
.loader > span:nth-child(2) {
  --n: 2;
}
.loader > span:nth-child(3) {
  --n: 3;
}
.loader > span:nth-child(4) {
  --n: 4;
}
.loader > span:nth-child(5) {
  --n: 5;
}
.loader > span:nth-child(6) {
  --n: 6;
}
.loader > span:nth-child(7) {
  --n: 7;
}
.loader > span:nth-child(8) {
  --n: 8;
}
.loader > span:nth-child(9) {
  --n: 9;
}
.loader > span:nth-child(10) {
  --n: 10;
}
</style>

2.封裝第二個組件錄音組件Audio.vue

這個組件依賴另一個文件recorder.js,在最下方貼出來瞭。

<template>
  <div>
    <el-button @click="myrecording" style="margin-left: 1rem">{{
      time
    }}</el-button>
    <el-button @click="startPlay" style="margin-left: 1rem">{{
      playing ? "播放" : "暫停"
    }}</el-button>
    <el-button @click="delvioce" style="margin-left: 1rem; color: black"
      >刪除</el-button
    >
    <audio
      v-if="fileurl"
      :src="fileurl"
      controls="controls"
      style="display: none"
      ref="audio"
      id="myaudio"
    ></audio>
  </div>
</template>
<script>
// 引入recorder.js
import recording from "@/utils/recorder"
export default {
  data () {
    return {
      RecordingSwitch: true, //錄音開關
      files: "", //語音文件
      num: 60, // 按住說話時間
      recorder: null,
      fileurl: "", //語音URL
      interval: "", //定時器
      time: "點擊說話(60秒)",
      playing: true,
    }
  },
 
  methods: {
    // 點擊錄制
    myrecording () {
      if (this.files === "") {
        if (this.RecordingSwitch) {
          this.Start()
        } else {
          this.End()
        }
      } else if (this.time === "點擊重錄(60秒)") {
        this.files = ""
        this.Start()
      }
      this.RecordingSwitch = !this.RecordingSwitch
    },
    // 點擊播放
    startPlay () {
      console.dir(this.$refs.audio, '--------------------')
      if (this.playing) {
        this.$refs.audio.play()
      } else {
        this.$refs.audio.pause()
      }
      this.playing = !this.playing
    },
    // 刪除語音
    delvioce () {
      this.fileurl = ""
      this.files = ""
      this.num = 60
      this.time = "點擊說話(60秒)"
    },
    // 清除定時器
    clearTimer () {
      if (this.interval) {
        this.num = 60
        clearInterval(this.interval)
      }
    },
    // 開始錄制
    Start () {
      this.clearTimer()
      recording.get((rec) => {
        // 當首次按下時,要獲取瀏覽器的麥克風權限,所以這時要做一個判斷處理
        if (rec) {
          this.recorder = rec
          this.interval = setInterval(() => {
            if (this.num <= 0) {
              this.recorder.stop()
              this.num = 60
              this.End()
            } else {
              this.time = "點擊結束(" + this.num + "秒)"
              this.recorder.start()
              this.num--
            }
          }, 1000)
        }
      })
    },
    // 停止錄制
    End () {
      this.clearTimer()
      if (this.recorder) {
        this.recorder.stop() // 重置說話時間
        this.num = 60
        this.time = "點擊重錄(60秒)" // 獲取語音二進制文件
        let bold = this.recorder.getBlob() // 將獲取的二進制對象轉為二進制文件流
        let files = new File([bold], "test.wav", {
          type: "audio/wav",
          lastModified: Date.now(),
        })
        this.files = files
        console.log(this.files, '----------', bold, '---------------', this.recorder)
        //獲取音頻時長
        let fileurl = URL.createObjectURL(files)
        this.fileurl = fileurl
        let audioElement = new Audio(fileurl)
        let duration
        audioElement.addEventListener("loadedmetadata", function (_event) {
          duration = audioElement.duration
          console.log("視頻時長:" + duration, files)
        })
        var downloadAnchorNode = document.createElement('a')
        downloadAnchorNode.setAttribute("href", fileurl)
        downloadAnchorNode.setAttribute("download", 'ssss')
        downloadAnchorNode.click()
        downloadAnchorNode.remove()
        this.$message.success("正在下載中,請稍後...")
      }
    },
  },
};
</script>
<style lang="less" >
</style>

3.recorder.js

// 兼容
window.URL = window.URL || window.webkitURL
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
let HZRecorder = function (stream, config) {
  config = config || {}
  config.sampleBits = config.sampleBits || 8 // 采樣數位 8, 16
  config.sampleRate = config.sampleRate || (16000) // 采樣率(1/6 44100)
  let context = new (window.webkitAudioContext || window.AudioContext)()
  let audioInput = context.createMediaStreamSource(stream)
  let createScript = context.createScriptProcessor || context.createJavaScriptNode
  let recorder = createScript.apply(context, [4096, 1, 1])
  let 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 () { // 合並壓縮
      // 合並
      let 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
      }
      // 壓縮
      let compression = parseInt(this.inputSampleRate / this.outputSampleRate)
      let length = data.length / compression
      let result = new Float32Array(length)
      let index = 0; let j = 0
      while (index < length) {
        result[index] = data[j]
        j += compression
        index++
      }
      return result
    },
    encodeWAV: function () {
      let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate)
      let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits)
      let bytes = this.compress()
      let dataLength = bytes.length * (sampleBits / 8)
      let buffer = new ArrayBuffer(44 + dataLength)
      let data = new DataView(buffer)
      let channelCount = 1// 單聲道
      let offset = 0
      let writeString = function (str) {
        for (let 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 (let i = 0; i < bytes.length; i++, offset++) {
          let s = Math.max(-1, Math.min(1, bytes[i]))
          let val = s < 0 ? s * 0x8000 : s * 0x7FFF
          val = parseInt(255 / (65535 / (val + 32768)))
          data.setInt8(offset, val, true)
        }
      } else {
        for (let i = 0; i < bytes.length; i++, offset += 2) {
          let 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/mp3' })
    }
  }
  // 開始錄音
  this.start = function () {
    audioInput.connect(recorder)
    recorder.connect(context.destination)
  }
  // 停止
  this.stop = function () {
    recorder.disconnect()
  }
  // 獲取音頻文件
  this.getBlob = function () {
    this.stop()
    return audioData.encodeWAV()
  }
  // 回放
  this.play = function (audio) {
    let downRec = document.getElementById('downloadRec')
    downRec.href = window.URL.createObjectURL(this.getBlob())
    downRec.download = new Date().toLocaleString() + '.mp3'
    audio.src = window.URL.createObjectURL(this.getBlob())
  }
  // 上傳
  this.upload = function (url, callback) {
    let fd = new FormData()
    fd.append('audioData', this.getBlob())
    let xhr = new XMLHttpRequest()
    /* eslint-disable */
    if (callback) {
      xhr.upload.addEventListener('progress', function (e) {
        callback('uploading', e)
      }, false)
      xhr.addEventListener('load', function (e) {
        callback('ok', e)
      }, false)
      xhr.addEventListener('error', function (e) {
        callback('error', e)
      }, false)
      xhr.addEventListener('abort', function (e) {
        callback('cancel', e)
      }, false)
    }
    /* eslint-disable */
    xhr.open('POST', url)
    xhr.send(fd)
  }
  // 音頻采集
  recorder.onaudioprocess = function (e) {
    audioData.input(e.inputBuffer.getChannelData(0))
    // record(e.inputBuffer.getChannelData(0));
  }
}
// 拋出異常
HZRecorder.throwError = function (message) {
  alert(message)
  throw new function () { this.toString = function () { return message } }()
}
// 是否支持錄音
HZRecorder.canRecording = (navigator.getUserMedia != null)
// 獲取錄音機
HZRecorder.get = function (callback, config) {
  if (callback) {
    if (navigator.getUserMedia) {
      console.log(navigator)
      navigator.getUserMedia(
        { audio: true } // 隻啟用音頻
        , function (stream) {
          let rec = new HZRecorder(stream, config)
          callback(rec)
        }
        , function (error) {
          console.log(error)
          switch (error.code || error.name) {
            case 'PERMISSION_DENIED':
            case 'PermissionDeniedError':
              HZRecorder.throwError('用戶拒絕提供信息。')
              break
            case 'NOT_SUPPORTED_ERROR':
            case 'NotSupportedError':
              HZRecorder.throwError('瀏覽器不支持硬件設備。')
              break
            case 'MANDATORY_UNSATISFIED_ERROR':
            case 'MandatoryUnsatisfiedError':
              HZRecorder.throwError('無法發現指定的硬件設備。')
              break
            default:
              HZRecorder.throwError('無法打開麥克風。異常信息:' + (error.code || error.name))
              break
          }
        })
    } else {
      HZRecorder.throwErr('當前瀏覽器不支持錄音功能。'); return
    }
  }
}
export default HZRecorder

以上就是Vue實現懸浮框自由移動+錄音功能的示例代碼的詳細內容,更多關於Vue懸浮框移動 錄音的資料請關註WalkonNet其它相關文章!

推薦閱讀: