index.tsx 17 KB

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