<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { KeyboardVoiceOutlined, KeyboardAltOutlined } from "@vicons/material";
import axios from "axios";
import { Icon } from "@vicons/utils";
import Recorder from "recorder-core";
import "recorder-core/src/engine/wav";
import "recorder-core/src/extensions/frequency.histogram.view";
import "recorder-core/src/extensions/lib.fft";

const emit = defineEmits(["startRecord", "stopRecord"]);

const isTalking = ref(false);
const sessionId = ref("");

const userTalk = ref("");

let rec;
let testSampleRate = ref(16000);
let testBitRate = ref(16);
let SendInterval = 300; // 控制实时传输间隔

let realTimeSendTryType,
  realTimeSendTryEncBusy,
  realTimeSendTryTime = 0,
  realTimeSendTryNumber,
  transferUploadNumberMax,
  realTimeSendTryChunk,
  voicePkgSeq = 0,
  wave = null;

const RealTimeSendTryReset = (type) => {
  realTimeSendTryType = type;
  realTimeSendTryTime = 0;
  realTimeSendTryEncBusy = 0;
  realTimeSendTryNumber = 0;
  transferUploadNumberMax = 0;
  realTimeSendTryChunk = null;
};

const RealTimeSendTry = (buffers, bufferSampleRate, isClose) => {
  const t1 = Date.now();
  if (realTimeSendTryTime === 0) {
    realTimeSendTryTime = t1;
    realTimeSendTryEncBusy = 0;
    realTimeSendTryNumber = 0;
    transferUploadNumberMax = 0;
    realTimeSendTryChunk = null;
  }

  if (!isClose && t1 - realTimeSendTryTime < SendInterval) {
    return; // 控制缓冲达到指定间隔才进行传输
  }

  realTimeSendTryTime = t1;
  const number = ++realTimeSendTryNumber;

  let pcm = [],
    pcmSampleRate = 0;
  if (buffers.length > 0) {
    // 借用SampleData函数进行数据的连续处理，采样率转换是顺带的，得到新的pcm数据
    const chunk = Recorder.SampleData(
      buffers,
      bufferSampleRate,
      testSampleRate.value,
      realTimeSendTryChunk,
      { frameType: isClose ? "" : realTimeSendTryType }
    );

    for (
      let i = realTimeSendTryChunk ? realTimeSendTryChunk.index : 0;
      i < chunk.index;
      i++
    ) {
      buffers[i] = null; // 清理已处理完的缓冲数据
    }

    realTimeSendTryChunk = chunk;
    pcm = chunk.data;
    pcmSampleRate = chunk.sampleRate;
  }

  // 如果没有新数据，或者结束时数据量太小，不能进行转码
  if (pcm.length === 0 || (isClose && pcm.length < 2000)) {
    TransferUpload(number, null, 0, null, isClose);
    return;
  }

  // 实时编码队列阻塞处理
  if (!isClose) {
    if (realTimeSendTryEncBusy >= 2) {
      console.log("编码队列阻塞，已丢弃一帧", 1);
      return;
    }
  }
  realTimeSendTryEncBusy++;

  const encStartTime = Date.now();
  const recMock = Recorder({
    type: realTimeSendTryType,
    sampleRate: testSampleRate.value, // 采样率
    bitRate: testBitRate.value, // 比特率
  });

  recMock.mock(pcm, pcmSampleRate);
  recMock.stop(
    (blob, duration) => {
      if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
      blob.encTime = Date.now() - encStartTime;
      TransferUpload(number, blob, duration, recMock, isClose);
    },
    (msg) => {
      if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
      console.log("不应该出现的错误:" + msg, 1);
    }
  );
};

const TransferUpload = (number, blobOrNull, duration, blobRec, isClose) => {
  transferUploadNumberMax = Math.max(transferUploadNumberMax, number);
  if (blobOrNull) {
    let blob = blobOrNull;
    let encTime = blob.encTime;
    // 发送数据的方式一：Base64文本发送
    let reader = new FileReader();
    reader.onloadend = () => {
      let base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1];
      // 可以实现 WebSocket send(base64), WebRTC send(base64), XMLHttpRequest send(base64)
      asrPost("SESSION_IN", base64);
    };
    reader.readAsDataURL(blob);
  }

  if (isClose) {
    console.log(
      "No." +
        (number < 100 ? ("000" + number).substr(-3) : number) +
        ":已停止传输"
    );
  }
};
const makeRandomString = (len) => {
  len = len || 32;
  let $chars =
    "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  let maxPos = $chars.length;
  let pwd = "";
  for (let i = 0; i < len; i++) {
    pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return pwd + new Date().getTime();
};
// 语音识别
const asrPost = async (eventCodeType = "SESSION_IN", base64) => {
  const res = await axios.post(
    `/openapi-stg/ai/voice/asr/v1`,
    {
      sessionId: sessionId.value,
      eventCodeType,
      voicePkgSeq: ++voicePkgSeq,
      audio: base64,
      format: 16000,
      encoding: "WAV",
    },
    {
      headers: {
        "X-Ai-Asr-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
      },
    }
  );

  console.log(res.data.body.asrResultText);

  userTalk.value = userTalk.value + res.data.body.asrResultText;
};

onMounted(() => {
  sessionId.value = makeRandomString(5);
  if (rec) {
    rec.close();
  }

  rec = Recorder({
    type: "unknown",
    onProcess: (buffers, powerLevel, bufferDuration, bufferSampleRate) => {
      RealTimeSendTry(buffers, bufferSampleRate, false);
      wave &&
        buffers[buffers.length - 1] &&
        wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
    },
  });

  const t = setTimeout(() => {
    console.log("无法录音：权限请求被忽略（超时假装手动点击了确认对话框）", 1);
  }, 8000);

  rec.open(
    () => {
      if (Recorder.FrequencyHistogramView) {
        wave = Recorder.FrequencyHistogramView({
          elem: ".recwave",
          lineCount: 10,
          position: 0,
          minHeight: 1,
          fallDuration: 400,
          stripeEnable: false,
          mirrorEnable: true,
          linear: [0, "#fff", 1, "#fff"],
        });
      }

      clearTimeout(t);
    },
    (msg, isUserNotAllow) => {
      clearTimeout(t);
      console.log(
        (isUserNotAllow ? "UserNotAllow，" : "") + "无法录音:" + msg,
        1
      );
    }
  );
  //console.log(document.querySelector(".recwave"));
  //const div = document.createElement("div");
  // const div = document.querySelector(".wave");
  // div.innerHTML =
  //   '<div style="height:100px;width:100%;" class="recwave"></div>';
  //document.body.prepend(div);
});

onBeforeUnmount(() => {
  if (rec) rec.close();
});

const startRecording = async (e) => {
  e.preventDefault();
  isTalking.value = true;
  userTalk.value = "";
  asrPost("SESSION_BEGIN");
  rec.start();
  RealTimeSendTryReset("wav");
  emit("startRecord");
};

const stopRecording = async () => {
  isTalking.value = false;
  //等待500ms后，在执行以下代码
  await new Promise((resolve) => setTimeout(resolve, 1000));
  rec.stop();
  RealTimeSendTry([], 0, true); // 最后一次发送
  await asrPost("SESSION_END");
  voicePkgSeq = 0;
  emit("stopRecord", userTalk.value);
};
</script>

<template>
  <div
    :class="`bottom-btn ${isTalking ? 'touch-start' : ''}`"
    @touchstart="startRecording"
    @touchend="stopRecording"
  >
    <div class="btn">
      <div class="text">按住说话</div>
      <Icon class="icon" color="white"><KeyboardAltOutlined /></Icon>
    </div>
    <div class="talk-item">
      <div class="wave">
        <div class="recwave" style="height: 100%; width: 100%"></div>
      </div>
      <Icon class="icon" color="#1989fa"><KeyboardVoiceOutlined /></Icon>
      <div>松手发送</div>
    </div>
  </div>
</template>

<style scoped>
.bottom-btn {
  position: fixed;
  bottom: 0;
  right: 0;
  width: 120vw;
  left: -10vw;
  display: flex;
  justify-content: center;
  height: 80px;
  background: rgba(255, 255, 255, 0);
  transition: height 0.1s, background 0.1s, border-radius 0.1s;
  padding-bottom: env(safe-area-inset-bottom);
}
.bottom-btn.touch-start {
  background: rgba(255, 255, 255, 0.8);
  border-radius: 50% 50% 0 0;
  height: 120px;
}
.bottom-btn.touch-start .btn {
  display: none;
}
.bottom-btn.touch-start .talk-item {
  visibility: visible;
}
.btn {
  width: 90vw;
  height: 40px;
  background: rgba(150, 150, 150, 0.5);
  color: white;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: absolute;
  bottom: calc(20px + env(safe-area-inset-bottom));
}
.btn .text {
  flex-grow: 1;
  text-align: center;
}
.btn .icon {
  margin-right: 10px;
}

.talk-item {
  position: absolute;
  bottom: calc(40px + env(safe-area-inset-bottom));
  visibility: hidden;
  text-align: center;
  color: #1989fa;
}
.talk-item .wave {
  position: absolute;
  top: -100px;
  left: 50%;
  margin-left: -50px;
  width: 100px;
  height: 50px;
  background: #1989fa;
  border-radius: 8px;
}
.talk-item .wave::after {
  content: "";
  position: absolute;
  bottom: -8px;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 8px solid rgb(25, 137, 250);
}
.talk-item .icon {
  font-size: 24px;
}
</style>
