index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import styles from "./index.module.less";
  2. import { Snackbar } from "@varlet/ui";
  3. import { closeToast, showLoadingToast, showToast, Popup } from "vant";
  4. import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
  5. import { getLeveByScore, getLeveByScoreMeasure, IEvaluatings } from "./evaluatResult";
  6. import {
  7. cancelEvaluating,
  8. endEvaluating,
  9. endSoundCheck,
  10. getEarphone,
  11. api_proxyServiceMessage,
  12. removeResult,
  13. sendResult,
  14. startEvaluating,
  15. startSoundCheck,
  16. api_openWebView,
  17. api_startRecording,
  18. api_stopRecording,
  19. api_recordStartTime,
  20. api_remove_recordStartTime,
  21. api_startCapture,
  22. api_endCapture,
  23. api_getDeviceDelay,
  24. hideComplexButton,
  25. api_checkSocketStatus,
  26. addAccompanyError,
  27. removeAccompanyError,
  28. addSocketStatus,
  29. removeSocketStatus,
  30. api_disconnectSocket,
  31. } from "/src/helpers/communication";
  32. import state, {
  33. IPlayState,
  34. clearSelection,
  35. handleStopPlay,
  36. onPlay,
  37. resetPlaybackToStart,
  38. togglePlay,
  39. } from "/src/state";
  40. import { IPostMessage } from "/src/utils/native-message";
  41. import { usePageVisibility } from "@vant/use";
  42. import { browser } from "/src/utils";
  43. import { getAudioCurrentTime, toggleMutePlayAudio } from "../audio-list";
  44. import { handleStartTick } from "../tick";
  45. import AbnormalPop from "../abnormal-pop";
  46. import { storeData } from "../../store";
  47. const browserInfo = browser();
  48. let socketStartTime = 0
  49. export const evaluatingData = reactive({
  50. /** 评测数据 */
  51. contentData: {} as any,
  52. /** 评测模块是否加载完成 */
  53. rendered: false,
  54. earphone: false, // 是否插入耳机
  55. soundEffect: false, // 是否效音
  56. soundEffectFrequency: 0, // 效音频率
  57. checkStep: 0, // 执行步骤
  58. checkEnd: false, // 检测结束
  59. earphoneMode: false, // 耳机弹窗
  60. soundEffectMode: false, // 效音弹窗
  61. websocketState: false, // websocket连接状态
  62. /**是否开始播放 */
  63. startBegin: false, // 开始
  64. backtime: 0, // 延迟时间
  65. /** 已经评测的数据 */
  66. evaluatings: {} as IEvaluatings,
  67. /** 评测结果 */
  68. resultData: {} as any,
  69. /** 评测结果弹窗 */
  70. resulstMode: false,
  71. /** 是否是完整评测 */
  72. isComplete: false,
  73. /** */
  74. isDisabledPlayMusic: false,
  75. /** socket异常状态弹窗 */
  76. socketErrorPop: false,
  77. /** 异常提示 */
  78. errorContents: '',
  79. /** socket异常状态弹窗的状态值 */
  80. socketErrorStatus: 0,
  81. /** 延迟检测,socket状态异常 */
  82. delayCheckSocketError: false,
  83. });
  84. /** 点击开始评测按钮 */
  85. export const handleStartEvaluat = async () => {
  86. if (state.modeType === "evaluating") {
  87. handleCancelEvaluat();
  88. } else {
  89. if (state.platform !== 'PC') {
  90. // 评测前先检查APP端的websocket状态
  91. const res = await api_checkSocketStatus();
  92. if (res?.content?.status === "connected") {
  93. handleStopPlay();
  94. } else {
  95. // socket未连接
  96. // evaluatingData.socketErrorPop = true
  97. }
  98. } else {
  99. handleStopPlay();
  100. }
  101. }
  102. state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating";
  103. if (state.modeType !== "evaluating") {
  104. // 切换到练习模式,卸载评测模块
  105. evaluatingData.rendered = false;
  106. }
  107. };
  108. /** 开始评测 & 延迟检测开始按钮 */
  109. export const startCheckDelay = async () => {
  110. // 评测前先检查APP端的websocket状态
  111. const res = await api_checkSocketStatus();
  112. if (res?.content?.status === "connected") {
  113. //
  114. return new Promise((resolve) => {
  115. resolve({checked: true})
  116. });
  117. } else {
  118. /**
  119. * socket未连接,记录此时的时间,以便于和收到socket成功链接,进行对比,对比时间小于500ms时,则连接中的状态默认显示500ms持续时间
  120. *
  121. * */
  122. socketStartTime = +new Date()
  123. evaluatingData.socketErrorPop = true
  124. evaluatingData.socketErrorStatus = 1
  125. return new Promise((resolve) => {
  126. resolve({checked: false})
  127. });
  128. }
  129. }
  130. const check_currentTime = () => {
  131. let preTime = 0;
  132. // 选段评测模式
  133. if (state.isSelectMeasureMode) {
  134. preTime = state.section[0].time * 1000;
  135. }
  136. const currentTime = getAudioCurrentTime() * 1000 - preTime;
  137. // console.log('播放进度music', currentTime, 'preTime:' + preTime)
  138. if (currentTime >= 500) {
  139. sendEvaluatingOffsetTime(500);
  140. return;
  141. }
  142. setTimeout(() => {
  143. check_currentTime();
  144. }, 10);
  145. };
  146. /** 开始播放发送延迟时间 */
  147. export const sendEvaluatingOffsetTime = async (currentTime: number) => {
  148. // 没有开始时间点, 不处理
  149. if (!evaluatingData.backtime) return;
  150. const nowTime = Date.now();
  151. const delayTime = nowTime - evaluatingData.backtime - currentTime;
  152. console.error("真正播放延迟", delayTime, "currentTime:", currentTime);
  153. await api_proxyServiceMessage({
  154. header: {
  155. commond: "audioPlayStart",
  156. type: "SOUND_COMPARE",
  157. },
  158. body: {
  159. offsetTime: delayTime < 0 ? 0 : delayTime,
  160. micDelay: 0,
  161. },
  162. });
  163. };
  164. /** 检测耳机 */
  165. const checkUseEarphone = async () => {
  166. const res = await getEarphone();
  167. return res?.content?.checkIsWired || false;
  168. };
  169. /**
  170. * 开始录音
  171. */
  172. const handleStartSoundCheck = () => {
  173. startSoundCheck();
  174. };
  175. /** 结束录音 */
  176. export const handleEndSoundCheck = () => {
  177. endSoundCheck();
  178. };
  179. /** 连接websocket */
  180. export const connectWebsocket = async (content: any) => {
  181. evaluatingData.contentData = content;
  182. evaluatingData.websocketState = true;
  183. };
  184. /**
  185. * 执行检测
  186. */
  187. export const handlePerformDetection = async () => {
  188. // 检测完成不检测了
  189. if (evaluatingData.checkEnd) return;
  190. // 延迟检测
  191. if (evaluatingData.checkStep === 0) {
  192. evaluatingData.checkStep = 5;
  193. // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态
  194. if (state.setting.soundEffect) {
  195. evaluatingData.soundEffectMode = true;
  196. return;
  197. }
  198. const delayTime = await api_getDeviceDelay();
  199. console.log("🚀 ~ delayTime:", delayTime);
  200. if (!delayTime) {
  201. evaluatingData.soundEffectMode = true;
  202. return;
  203. }
  204. handlePerformDetection();
  205. return;
  206. }
  207. // 检测耳机
  208. if ((evaluatingData.checkStep = 5)) {
  209. evaluatingData.checkStep = 10;
  210. const erji = await checkUseEarphone();
  211. if (!erji) {
  212. evaluatingData.earphoneMode = true;
  213. return;
  214. }
  215. handlePerformDetection();
  216. return;
  217. }
  218. // 效音
  219. // if (evaluatingData.checkStep === 7) {
  220. // // 是否需要开启效音
  221. // evaluatingData.checkStep = 10;
  222. // if (state.setting.soundEffect && !state.isPercussion) {
  223. // evaluatingData.soundEffectMode = true;
  224. // handleStartSoundCheck();
  225. // return
  226. // }
  227. // handlePerformDetection();
  228. // return;
  229. // }
  230. // 效验完成
  231. if (evaluatingData.checkStep === 10) {
  232. evaluatingData.checkEnd = true;
  233. }
  234. };
  235. /** 记录小节分数 */
  236. export const addMeasureScore = (measureScore: any, show = true) => {
  237. // #8720 bug修复
  238. for(let idx in evaluatingData.evaluatings) {
  239. evaluatingData.evaluatings[idx].show = false
  240. }
  241. evaluatingData.evaluatings[measureScore.measureRenderIndex] = {
  242. ...measureScore,
  243. leve: getLeveByScoreMeasure(measureScore.score),
  244. show,
  245. };
  246. // console.log("🚀 ~ measureScore:", evaluatingData.evaluatings)
  247. };
  248. const handleScoreResult = (res?: IPostMessage) => {
  249. console.log('返回', res)
  250. if (res?.content) {
  251. const { header, body } = res.content;
  252. // 效音返回
  253. if (header.commond === "checking") {
  254. evaluatingData.soundEffectFrequency = body.frequency;
  255. }
  256. // 小节评分返回
  257. if (header?.commond === "measureScore") {
  258. console.log("🚀 ~ 评测返回:", res);
  259. addMeasureScore(body);
  260. }
  261. // 评测结束返回
  262. if (header?.commond === "overall") {
  263. console.log("🚀 ~ 评测返回:", res);
  264. // console.log("评测结束", body);
  265. state.isHideEvaluatReportSaveBtn = false;
  266. evaluatingData.resulstMode = true;
  267. evaluatingData.resultData = {
  268. ...body,
  269. ...getLeveByScore(body.score),
  270. };
  271. // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData)
  272. closeToast();
  273. }
  274. }
  275. };
  276. /** 开始评测 */
  277. export const handleStartBegin = async (preTimes?: number) => {
  278. evaluatingData.isComplete = false;
  279. evaluatingData.evaluatings = {};
  280. evaluatingData.resultData = {};
  281. evaluatingData.backtime = 0;
  282. resetPlaybackToStart();
  283. const res = await startEvaluating(evaluatingData.contentData);
  284. if (res?.api !== "startEvaluating") {
  285. Snackbar.error("请在APP端进行评测");
  286. evaluatingData.startBegin = false;
  287. return;
  288. }
  289. if (res?.content?.reson) {
  290. showToast(res.content?.des);
  291. evaluatingData.startBegin = false;
  292. return;
  293. }
  294. evaluatingData.startBegin = true;
  295. if (evaluatingData.isDisabledPlayMusic) {
  296. state.playState = state.playState === "paused" ? "play" : "paused";
  297. // 设置为开始播放时, 如果需要节拍,先播放节拍器
  298. if (state.playState === "play" && state.needTick) {
  299. const tickend = await handleStartTick();
  300. // console.log("🚀 ~ tickend:", tickend)
  301. // 节拍器返回false, 取消播放
  302. if (!tickend) {
  303. state.playState = "paused";
  304. evaluatingData.startBegin = false;
  305. return;
  306. }
  307. }
  308. onPlay();
  309. }
  310. //开始录音
  311. await api_startRecording({
  312. accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
  313. firstNoteTime: preTimes || 0,
  314. });
  315. // 如果开启了摄像头, 开启录制视频
  316. if (state.setting.camera) {
  317. console.log("开始录制视频");
  318. api_startCapture();
  319. }
  320. };
  321. /** 播放音乐 */
  322. const playMusic = async () => {
  323. const playState = await togglePlay("play");
  324. // 取消播放,停止播放
  325. if (!playState) {
  326. evaluatingData.startBegin = false;
  327. handleCancelEvaluat();
  328. return;
  329. }
  330. // 检测播放进度, 计算延迟
  331. check_currentTime();
  332. // 如果开启了摄像头, 开启录制视频
  333. if (state.setting.camera) {
  334. console.log("开始录制视频");
  335. api_startCapture();
  336. }
  337. };
  338. let _audio: HTMLAudioElement;
  339. /** 录音开始,记录开始时间点 */
  340. const recordStartTimePoint = async (res?: IPostMessage) => {
  341. console.error("开始录音");
  342. // 没有开始评测,不处理
  343. if (!evaluatingData.startBegin) return;
  344. let inteveral = res?.content?.inteveral || 0;
  345. if (browserInfo.ios) {
  346. inteveral *= 1000;
  347. }
  348. evaluatingData.backtime = inteveral || Date.now();
  349. console.log(
  350. "🚀 ~ 开始时间点:",
  351. evaluatingData.backtime,
  352. "已经录的时间:",
  353. Date.now() - inteveral,
  354. "记录时间点:",
  355. Date.now()
  356. );
  357. // 是否禁播
  358. if (evaluatingData.isDisabledPlayMusic) {
  359. return;
  360. }
  361. // 开始播放
  362. playMusic();
  363. };
  364. /**
  365. * 结束评测
  366. * @param isComplete 是否完整评测
  367. * @returns
  368. */
  369. export const handleEndEvaluat = (isComplete = false) => {
  370. // 没有开始评测 , 不是评测模式 , 不评分
  371. if (!evaluatingData.startBegin || state.modeType !== "evaluating") return;
  372. // 结束录音
  373. // api_stopRecording();
  374. // 结束评测
  375. endEvaluating({
  376. musicScoreId: state.examSongId,
  377. });
  378. showLoadingToast({
  379. message: "评分中",
  380. duration: 0,
  381. overlay: true,
  382. overlayClass: styles.scoreMode,
  383. });
  384. setTimeout(() => {
  385. evaluatingData.startBegin = false;
  386. }, 500);
  387. evaluatingData.isComplete = isComplete;
  388. // 如果开启了摄像头, 结束录制视频
  389. if (state.setting.camera) {
  390. console.log("结束录制视频");
  391. api_endCapture();
  392. }
  393. };
  394. /**
  395. * 结束评测
  396. */
  397. export const handleEndBegin = () => {
  398. handleEndEvaluat();
  399. handleStopPlay();
  400. };
  401. /**
  402. * 取消评测
  403. */
  404. export const handleCancelEvaluat = () => {
  405. evaluatingData.evaluatings = {};
  406. evaluatingData.startBegin = false;
  407. // 关闭提示
  408. closeToast();
  409. // 取消记录
  410. api_proxyServiceMessage({
  411. header: {
  412. commond: "recordCancel",
  413. type: "SOUND_COMPARE",
  414. status: 200,
  415. },
  416. });
  417. // 取消评测
  418. cancelEvaluating();
  419. // 停止播放
  420. handleStopPlay();
  421. endEvaluating({
  422. musicScoreId: state.examSongId,
  423. });
  424. // 如果开启了摄像头, 结束录制视频
  425. if (state.setting.camera) {
  426. console.log("结束录制视频");
  427. api_endCapture();
  428. }
  429. };
  430. /** 查看报告 */
  431. export const handleViewReport = (
  432. key: "recordId" | "recordIdStr",
  433. type: "gym" | "colexiu" | "orchestra" | "instrument"
  434. ) => {
  435. const id = evaluatingData.resultData?.[key] || "";
  436. let url = "";
  437. switch (type) {
  438. case "gym":
  439. url = location.origin + location.pathname + "#/report/" + id;
  440. break;
  441. case "orchestra":
  442. url = location.origin + location.pathname + "report-share.html?id=" + id;
  443. break;
  444. case "instrument":
  445. url = location.origin + location.pathname + "#/evaluat-report?id=" + id;
  446. break;
  447. default:
  448. url = location.origin + location.pathname + "report-share.html?id=" + id;
  449. break;
  450. }
  451. api_openWebView({
  452. url,
  453. orientation: 0,
  454. isHideTitle: true, // 此处兼容安卓,意思为隐藏全部头部
  455. statusBarTextColor: false,
  456. isOpenLight: true,
  457. c_orientation: 0,
  458. });
  459. };
  460. // 隐藏存演奏按钮
  461. const handleComplexButton = (res?: IPostMessage) => {
  462. console.log('监听是否隐藏保存按钮', res)
  463. if (res?.content) {
  464. const { header, body } = res.content;
  465. state.isHideEvaluatReportSaveBtn = true
  466. }
  467. };
  468. // 检测到APP发送的异常信息
  469. const handleAccompanyError = (res?: IPostMessage) => {
  470. console.log('异常信息返回', res)
  471. if (res?.content) {
  472. const { type, reson } = res.content;
  473. switch (type) {
  474. case "enterBackground":
  475. // App退到后台
  476. case "playError":
  477. // 播放异常
  478. case "socketError":
  479. // socket连接断开,评测中,则取消评测
  480. // 延迟检测中
  481. if (evaluatingData.soundEffectMode) {
  482. evaluatingData.socketErrorStatus = 0
  483. evaluatingData.delayCheckSocketError = true
  484. evaluatingData.socketErrorPop = type === "socketError" ? true : false
  485. // api_checkSocketStatus()
  486. return
  487. }
  488. // 评测中
  489. if (state.modeType === "evaluating" && evaluatingData.startBegin) {
  490. handleCancelEvaluat();
  491. }
  492. evaluatingData.socketErrorStatus = 0
  493. evaluatingData.socketErrorPop = type === "socketError" ? true : false
  494. break;
  495. case "recordError":
  496. // 录音异常
  497. break;
  498. default:
  499. break;
  500. }
  501. }
  502. };
  503. // 监测socket状态,是否已经成功连接
  504. const handleSocketStatus = (res?: IPostMessage) => {
  505. if (res?.content?.status === "connected") {
  506. const currentTime = +new Date()
  507. evaluatingData.delayCheckSocketError = false
  508. const diffTime = currentTime - socketStartTime
  509. if (diffTime < 1000) {
  510. const remainingTime = 1000 - diffTime
  511. console.log(remainingTime,99999)
  512. setTimeout(() => {
  513. evaluatingData.socketErrorStatus = 2
  514. }, remainingTime);
  515. }
  516. }
  517. }
  518. // 评测出现异常,再试一次
  519. const hanldeConfirmPop = async () => {
  520. api_checkSocketStatus();
  521. evaluatingData.socketErrorStatus = 1
  522. socketStartTime = +new Date()
  523. }
  524. // 关闭异常弹窗
  525. const hanldeClosePop = () => {
  526. evaluatingData.socketErrorPop = false
  527. evaluatingData.socketErrorStatus = 0
  528. }
  529. export default defineComponent({
  530. name: "evaluating",
  531. setup() {
  532. const pageVisibility = usePageVisibility();
  533. // 需要记录的数据
  534. const record_old_data = reactive({
  535. /** 指法 */
  536. finger: false,
  537. /** 原音伴奏 */
  538. play_mode: "" as IPlayState,
  539. /** 评测是否要伴奏 */
  540. enableAccompaniment: true,
  541. });
  542. /** 记录状态 */
  543. const hanlde_record = () => {
  544. // 取消指法
  545. record_old_data.finger = state.setting.displayFingering;
  546. state.setting.displayFingering = false;
  547. // 切换为伴奏
  548. record_old_data.play_mode = state.playSource;
  549. record_old_data.enableAccompaniment = state.setting.enableAccompaniment;
  550. // 如果关闭伴奏,评测静音
  551. if (!record_old_data.enableAccompaniment) {
  552. console.log("关闭伴奏");
  553. toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : "background", true);
  554. }
  555. };
  556. /** 还原状态 */
  557. const handle_reduction = () => {
  558. // 还原指法
  559. state.setting.displayFingering = record_old_data.finger;
  560. state.playSource = record_old_data.play_mode;
  561. // 如果关闭伴奏, 结束评测取消静音
  562. if (!record_old_data.enableAccompaniment) {
  563. toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : "background", false);
  564. }
  565. };
  566. watch(pageVisibility, (value) => {
  567. if (value == "hidden" && evaluatingData.startBegin) {
  568. // handleEndBegin();
  569. }
  570. });
  571. watch(
  572. () => evaluatingData.socketErrorStatus,
  573. () => {
  574. if (evaluatingData.socketErrorStatus === 2) {
  575. setTimeout(() => {
  576. evaluatingData.socketErrorPop = false
  577. // evaluatingData.socketErrorStatus = 0
  578. }, 1000);
  579. }
  580. }
  581. );
  582. onMounted(() => {
  583. resetPlaybackToStart();
  584. hanlde_record();
  585. evaluatingData.resultData = {};
  586. // evaluatingData.resulstMode = true;
  587. // evaluatingData.resultData = {...getLeveByScore(10), score: 10, intonation: 10, cadence: 30, integrity: 40}
  588. // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData)
  589. evaluatingData.evaluatings = {};
  590. evaluatingData.soundEffectFrequency = 0;
  591. evaluatingData.checkStep = 0;
  592. evaluatingData.rendered = true;
  593. sendResult(handleScoreResult);
  594. hideComplexButton(handleComplexButton, true);
  595. api_recordStartTime(recordStartTimePoint);
  596. addAccompanyError(handleAccompanyError);
  597. addSocketStatus(handleSocketStatus);
  598. // 不是选段模式评测, 就清空已选段
  599. if (!state.isSelectMeasureMode) {
  600. clearSelection();
  601. }
  602. console.log("加载评测模块成功");
  603. });
  604. onUnmounted(() => {
  605. evaluatingData.checkEnd = false;
  606. evaluatingData.rendered = false;
  607. resetPlaybackToStart();
  608. removeResult(handleScoreResult);
  609. hideComplexButton(() => {}, false);
  610. api_remove_recordStartTime(recordStartTimePoint);
  611. handle_reduction();
  612. removeAccompanyError(handleAccompanyError);
  613. removeSocketStatus(handleSocketStatus);
  614. api_disconnectSocket();
  615. console.log("卸载评测模块成功");
  616. });
  617. return () => (
  618. <div>
  619. <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.socketErrorPop}>
  620. <AbnormalPop
  621. onConfirm={hanldeConfirmPop}
  622. onClose={hanldeClosePop}
  623. />
  624. </Popup>
  625. </div>
  626. );
  627. },
  628. });