h5-record.vue 9.13 KB
<!--
 * @Date: 2024-02-01 11:11:21
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2024-02-01 15:03:08
 * @FilePath: /my-record/src/views/h5-record.vue
 * @Description: 文件描述
-->
<template>
  <div class="h5-record">
    <div class="wave-wrapper">
      <div class="ctrlProcessWave wave-view"></div>
      <div class="wave-info">
        <div class="ctrlProcessX wave-info-top" :style="{ width: powerLevel + '%' }" ></div>
        <div class="ctrlProcessT wave-info-bottom" >
          {{ durationTxt + "/" + powerLevel }}
        </div>
      </div>
    </div>

    <div style="margin: 20px 10px;">
      <audio ref="LogAudioPlayer" style="width: 100%"></audio>
    </div>

    <van-floating-panel>
      <van-cell-group>
        <van-cell title="打开录音,请求权限" @click.native="recOpen" />
        <van-cell title="关闭录音,释放资源" @click.native="recClose" />
        <van-cell title="录制" @click.native="recStart" />
        <van-cell title="暂停" @click.native="recPause" />
        <van-cell title="继续录音" @click.native="recResume" />
        <van-cell title="停止录音" @click.native="recStop" />
        <van-cell title="播放录音" @click.native="recPlayLast" />
        <van-cell title="下载录音" @click.native="recDownLast" />
        <van-cell title="上传录音" @click.native="recUploadLast" />
      </van-cell-group>
    </van-floating-panel>

  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'

import { Cookies, $, _, axios, storeToRefs, mainStore, Toast, useTitle } from '@/utils/generatePackage.js'
//import { } from '@/utils/generateModules.js'
//import { } from '@/utils/generateIcons.js'
//import { } from '@/composables'
import { v4 as uuidv4 } from 'uuid';
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
//加载必须要的core,demo简化起见采用的直接加载类库,实际使用时应当采用异步按需加载
import Recorder from "recorder-core";
//需要使用到的音频格式编码引擎的js文件统统加载进来,这些引擎文件会比较大
import "recorder-core/src/engine/mp3";
import "recorder-core/src/engine/mp3-engine";
//可选的扩展
import "recorder-core/src/extensions/waveview";

const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);

const type = ref("mp3"); // 音频类型
const bitRate = ref(32); // 比特率
const sampleRate = ref(32000); // 采样率
const duration = ref(0);
const durationTxt = ref("0");
const powerLevel = ref(0);
const logs = ref([]);
const recLogLast = ref({});
const LogAudioPlayer = ref(null);

const rec = ref(null); // 录音控件实例
const wave = ref(null); // 波形绘制对象

const formatMs = (ms, all) => { // 格式化时间显示
  var ss = ms % 1000;
  ms = (ms - ss) / 1000;
  var s = ms % 60;
  ms = (ms - s) / 60;
  var m = ms % 60;
  ms = (ms - m) / 60;
  var h = ms;
  var t =
    (h ? h + ":" : "") +
    (all || h + m ? ("0" + m).substr(-2) + ":" : "") +
    (all || h + m + s ? ("0" + s).substr(-2) + "″" : "") +
    ("00" + ss).substr(-3);
  return t;
}
/**
 * 录音日志
 * @param {String} msg 日志内容
 * @param {String} color 日志颜色
 * @param {Object} res 日志资源
 */
const reclog = (msg, color, res) => {
  var obj = {
    idx: logs.value.length,
    msg: msg,
    color: color,
    res: res,
    playMsg: "",
    down: 0,
    down64Val: "",
  };
  if (res && res.blob) {
    recLogLast.value = obj;
  }
  logs.value.splice(0, 0, obj);
}

/**
 * 打开录音
 */
const recOpen = () => {
  rec.value = Recorder({
    type: type.value,
    bitRate: +bitRate.value,
    sampleRate: +sampleRate.value,
    onProcess: function (buffers, p, d, sampleRate) { // 播放进程中
      duration.value = d;
      durationTxt.value = formatMs(d, 1);
      powerLevel.value = p;
      // 改变波纹显示
      wave.value.input(buffers[buffers.length - 1], p, sampleRate);
    },
  });

  rec.value.open(
    () => { // 成功回调
      reclog(
        "已打开:" + type.value + " " + sampleRate.value + "hz " + bitRate.value + "kbps",
        2
      );

      wave.value = Recorder.WaveView({ elem: ".ctrlProcessWave" });
    },
    (msg, isUserNotAllow) => { // 失败回调
      reclog((isUserNotAllow ? "UserNotAllow," : "") + "打开失败:" + msg, 1);
    }
  );
}
/**
 * 关闭录音
 */
const recClose = () => {
  if (rec.value) {
    rec.value.close();
    reclog("已关闭");
  } else {
    reclog("未打开录音", 1);
  }
}
/**
 * 开始录音
 */
const recStart = () => {
  if (!rec.value || !Recorder.IsOpen()) {
    reclog("未打开录音", 1);
    return;
  }
  rec.value.start();

  let set = rec.value.set;
  reclog(
    "录制中:" + set.type + " " + set.sampleRate + "hz " + set.bitRate + "kbps"
  );
}
/**
 * 暂停录音
 */
const recPause = () => {
  if (rec.value && Recorder.IsOpen()) {
    rec.value.pause();
  } else {
    reclog("未打开录音", 1);
  }
}
/**
 * 继续录音
 */
const recResume = () => {
  if (rec.value && Recorder.IsOpen()) {
    rec.value.resume();
  } else {
    reclog("未打开录音", 1);
  }
}
/**
 * 停止录音
 */
const recStop = () => {
  if (!(rec.value && Recorder.IsOpen())) {
    reclog("未打开录音", 1);
    return;
  }

  rec.value.stop(
    function (blob, duration) {
      reclog("已录制:", "", {
        blob: blob,
        duration: duration,
        durationTxt: formatMs(duration),
        rec: rec,
      });
    },
    function (s) {
      reclog("录音失败:" + s, 1);
    }
  );
}

const recPlay = (idx) => {
  let o = logs.value[logs.value.length - idx - 1];
  o.play = (o.play || 0) + 1;

  let audio = LogAudioPlayer.value;
  audio.controls = true;
  if (!(audio.ended || audio.paused)) {
    audio.pause();
  }
  audio.onerror = function (e) {
    console.warn("播放失败:" + e);
  };
  audio.src = (window.URL || webkitURL).createObjectURL(o.res.blob);
  audio.play();
}
/**
 * 播放录音
 */
const recPlayLast = () => {
  if (!recLogLast.value) {
    reclog("请先录音,然后停止后再播放", 1);
    return;
  }
  recPlay(recLogLast.value.idx);
}

const recDown = (idx) => {
  let o = logs.value[logs.value.length - idx - 1];
  o.down = (o.down || 0) + 1;

  o = o.res;
  let name =
    "rec-" +
    o.duration +
    "ms-" +
    (o.rec.set.bitRate || "-") +
    "kbps-" +
    (o.rec.set.sampleRate || "-") +
    "hz." +
    (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || "unknown");
  var downA = document.createElement("A");
  downA.href = (window.URL || webkitURL).createObjectURL(o.blob);
  downA.download = name;
  downA.click();
}
/**
 * 下载录音
 */
const recDownLast = () => {
  if (!recLogLast.value) {
    reclog("请先录音,然后停止后再下载", 1);
    return;
  }
  recDown(recLogLast.value.idx);
}
/**
 * 转换格式base64
 */
const recDown64 = (idx) => {
  let o = logs.value[logs.value.length - idx - 1];
  let reader = new FileReader();
  reader.onloadend = function () {
    o.down64Val = reader.result;
  };
  reader.readAsDataURL(o.res.blob);
}
/**
 * 上传录音
 */
const recUploadLast = () => {
  if (!recLogLast.value) {
    reclog("请先录音,然后停止后再上传", 1);
    return;
  }
  let blob = recLogLast.value.res.blob;

  let reader = new FileReader();
  reader.onloadend = async function () {
    let base64url = encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]); //录音文件内容,后端进行base64解码成二进制

    let affix = uuidv4();

    // TAG: 获取token和保存的接口应该有问题
    // 获取七牛token
    const { token, key, code } = await qiniuTokenAPI({ filename: `${affix}_my_record.mp3`, file: base64url });
    if (code) {
      let formData = new FormData();
      formData.append("file", base64url); // 通过append向form对象添加数据
      formData.append("token", token);
      formData.append("key", `${affix}_my_record.mp3`);
      let config = {
        headers: { "Content-Type": "multipart/form-data" },
      };
      // 自拍图片上传七牛服务器
      let qiniuUploadUrl;
      if (window.location.protocol === 'https:') {
        qiniuUploadUrl = 'https://up.qbox.me';
      } else {
        qiniuUploadUrl = 'http://upload.qiniu.com';
      }
      const { filekey, hash } = await qiniuUploadAPI(
        qiniuUploadUrl,
        formData,
        config
      );
      if (filekey) {
        const { data } = await saveFileAPI({ filekey, hash });
        // console.warn(filekey)
      }
    }
  };
  reader.readAsDataURL(blob);
}
</script>

<style lang="less" scoped>
.h5-record {
  background-color: #f9f9f9;
  height: 100vh;
  .wave-wrapper {
    .wave-view {
      height: 100px;
      width: 100vw;
      border: 1px solid #ccc;
      box-sizing: border-box;
      display: inline-block;
      vertical-align: bottom;
    }
    .wave-info {
      height: 40px;
      width: 100vw;
      display: inline-block;
      background: #999;
      position: relative;
      vertical-align: bottom;
      .wave-info-top {
        position: absolute;
        height: 40px;
        background: #0b1;
      }
      .wave-info-bottom {
        position: relative;
        padding-left: 50px;
        line-height: 40px;
      }
    }
  }
}
</style>