index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. import styles from "./index.module.less";
  2. import { Snackbar } from "@varlet/ui";
  3. import { closeToast, showLoadingToast, showToast } 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. } from "/src/helpers/communication";
  26. import state, {
  27. IPlayState,
  28. clearSelection,
  29. handleStopPlay,
  30. onPlay,
  31. resetPlaybackToStart,
  32. togglePlay,
  33. } from "/src/state";
  34. import { IPostMessage } from "/src/utils/native-message";
  35. import { usePageVisibility } from "@vant/use";
  36. import { browser } from "/src/utils";
  37. import { getAudioCurrentTime, toggleMutePlayAudio } from "../audio-list";
  38. import { handleStartTick } from "../tick";
  39. const browserInfo = browser();
  40. export const evaluatingData = reactive({
  41. /** 评测数据 */
  42. contentData: {} as any,
  43. /** 评测模块是否加载完成 */
  44. rendered: false,
  45. earphone: false, // 是否插入耳机
  46. soundEffect: false, // 是否效音
  47. soundEffectFrequency: 0, // 效音频率
  48. checkStep: 0, // 执行步骤
  49. checkEnd: false, // 检测结束
  50. earphoneMode: false, // 耳机弹窗
  51. soundEffectMode: false, // 效音弹窗
  52. websocketState: false, // websocket连接状态
  53. /**是否开始播放 */
  54. startBegin: false, // 开始
  55. backtime: 0, // 延迟时间
  56. /** 已经评测的数据 */
  57. evaluatings: {} as IEvaluatings,
  58. /** 评测结果 */
  59. resultData: {} as any,
  60. /** 评测结果弹窗 */
  61. resulstMode: false,
  62. /** 是否是完整评测 */
  63. isComplete: false,
  64. /** */
  65. isDisabledPlayMusic: false,
  66. });
  67. /** 点击开始评测按钮 */
  68. export const handleStartEvaluat = () => {
  69. if (state.modeType === "evaluating") {
  70. handleCancelEvaluat();
  71. } else {
  72. handleStopPlay();
  73. }
  74. state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating";
  75. if (state.modeType !== "evaluating") {
  76. // 切换到练习模式,卸载评测模块
  77. evaluatingData.rendered = false;
  78. }
  79. };
  80. const check_currentTime = () => {
  81. let preTime = 0;
  82. // 选段评测模式
  83. if (state.isSelectMeasureMode) {
  84. preTime = state.section[0].time * 1000;
  85. }
  86. const currentTime = getAudioCurrentTime() * 1000 - preTime;
  87. // console.log('播放进度music', currentTime, 'preTime:' + preTime)
  88. if (currentTime >= 500) {
  89. sendEvaluatingOffsetTime(500);
  90. return;
  91. }
  92. setTimeout(() => {
  93. check_currentTime();
  94. }, 10);
  95. };
  96. /** 开始播放发送延迟时间 */
  97. export const sendEvaluatingOffsetTime = async (currentTime: number) => {
  98. // 没有开始时间点, 不处理
  99. if (!evaluatingData.backtime) return;
  100. const nowTime = Date.now();
  101. const delayTime = nowTime - evaluatingData.backtime - currentTime;
  102. console.error("真正播放延迟", delayTime, "currentTime:", currentTime);
  103. await api_proxyServiceMessage({
  104. header: {
  105. commond: "audioPlayStart",
  106. type: "SOUND_COMPARE",
  107. },
  108. body: {
  109. offsetTime: delayTime < 0 ? 0 : delayTime,
  110. micDelay: 0,
  111. },
  112. });
  113. };
  114. /** 检测耳机 */
  115. const checkUseEarphone = async () => {
  116. const res = await getEarphone();
  117. return res?.content?.checkIsWired || false;
  118. };
  119. /**
  120. * 开始录音
  121. */
  122. const handleStartSoundCheck = () => {
  123. startSoundCheck();
  124. };
  125. /** 结束录音 */
  126. export const handleEndSoundCheck = () => {
  127. endSoundCheck();
  128. };
  129. /** 连接websocket */
  130. export const connectWebsocket = async (content: any) => {
  131. evaluatingData.contentData = content;
  132. evaluatingData.websocketState = true;
  133. };
  134. /**
  135. * 执行检测
  136. */
  137. export const handlePerformDetection = async () => {
  138. // 检测完成不检测了
  139. if (evaluatingData.checkEnd) return;
  140. // 延迟检测
  141. if (evaluatingData.checkStep === 0) {
  142. evaluatingData.checkStep = 5;
  143. // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态
  144. if (state.setting.soundEffect) {
  145. evaluatingData.soundEffectMode = true;
  146. return;
  147. }
  148. const delayTime = await api_getDeviceDelay();
  149. console.log("🚀 ~ delayTime:", delayTime);
  150. if (!delayTime) {
  151. evaluatingData.soundEffectMode = true;
  152. return;
  153. }
  154. handlePerformDetection();
  155. return;
  156. }
  157. // 检测耳机
  158. if ((evaluatingData.checkStep = 5)) {
  159. evaluatingData.checkStep = 10;
  160. const erji = await checkUseEarphone();
  161. if (!erji) {
  162. evaluatingData.earphoneMode = true;
  163. return;
  164. }
  165. handlePerformDetection();
  166. return;
  167. }
  168. // 效音
  169. // if (evaluatingData.checkStep === 7) {
  170. // // 是否需要开启效音
  171. // evaluatingData.checkStep = 10;
  172. // if (state.setting.soundEffect && !state.isPercussion) {
  173. // evaluatingData.soundEffectMode = true;
  174. // handleStartSoundCheck();
  175. // return
  176. // }
  177. // handlePerformDetection();
  178. // return;
  179. // }
  180. // 效验完成
  181. if (evaluatingData.checkStep === 10) {
  182. evaluatingData.checkEnd = true;
  183. }
  184. };
  185. /** 记录小节分数 */
  186. export const addMeasureScore = (measureScore: any, show = true) => {
  187. // #8720 bug修复
  188. for(let idx in evaluatingData.evaluatings) {
  189. evaluatingData.evaluatings[idx].show = false
  190. }
  191. evaluatingData.evaluatings[measureScore.measureRenderIndex] = {
  192. ...measureScore,
  193. leve: getLeveByScoreMeasure(measureScore.score),
  194. show,
  195. };
  196. // console.log("🚀 ~ measureScore:", evaluatingData.evaluatings)
  197. };
  198. const handleScoreResult = (res?: IPostMessage) => {
  199. console.log('返回', res)
  200. if (res?.content) {
  201. const { header, body } = res.content;
  202. // 效音返回
  203. if (header.commond === "checking") {
  204. evaluatingData.soundEffectFrequency = body.frequency;
  205. }
  206. const productNum = () => {
  207. return Math.floor(Math.random() * 20) + (80);
  208. }
  209. // 小节评分返回
  210. if (header?.commond === "measureScore") {
  211. console.log("🚀 ~ 评测返回:", res);
  212. // 录视频
  213. body.score = productNum();
  214. addMeasureScore(body);
  215. }
  216. // 评测结束返回
  217. if (header?.commond === "overall") {
  218. console.log("🚀 ~ 评测返回:", res);
  219. // console.log("评测结束", body);
  220. // 录视频
  221. body.score = productNum();
  222. body.intonation = productNum();
  223. body.cadence = productNum();
  224. body.integrity = productNum();
  225. state.isHideEvaluatReportSaveBtn = false;
  226. evaluatingData.resulstMode = true;
  227. evaluatingData.resultData = {
  228. ...body,
  229. ...getLeveByScore(body.score),
  230. };
  231. // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData)
  232. closeToast();
  233. }
  234. }
  235. };
  236. /** 开始评测 */
  237. export const handleStartBegin = async () => {
  238. evaluatingData.isComplete = false;
  239. evaluatingData.evaluatings = {};
  240. evaluatingData.resultData = {};
  241. evaluatingData.backtime = 0;
  242. resetPlaybackToStart();
  243. const res = await startEvaluating(evaluatingData.contentData);
  244. if (res?.api !== "startEvaluating") {
  245. Snackbar.error("请在APP端进行评测");
  246. evaluatingData.startBegin = false;
  247. return;
  248. }
  249. if (res?.content?.reson) {
  250. showToast(res.content.reson);
  251. evaluatingData.startBegin = false;
  252. return;
  253. }
  254. evaluatingData.startBegin = true;
  255. if (evaluatingData.isDisabledPlayMusic) {
  256. state.playState = state.playState === "paused" ? "play" : "paused";
  257. // 设置为开始播放时, 如果需要节拍,先播放节拍器
  258. if (state.playState === "play" && state.needTick) {
  259. const tickend = await handleStartTick();
  260. // console.log("🚀 ~ tickend:", tickend)
  261. // 节拍器返回false, 取消播放
  262. if (!tickend) {
  263. state.playState = "paused";
  264. evaluatingData.startBegin = false;
  265. return;
  266. }
  267. }
  268. onPlay();
  269. }
  270. //开始录音
  271. await api_startRecording({
  272. accompanimentState: state.setting.enableAccompaniment ? 1 : 0
  273. });
  274. // 如果开启了摄像头, 开启录制视频
  275. if (state.setting.camera) {
  276. console.log("开始录制视频");
  277. api_startCapture();
  278. }
  279. };
  280. /** 播放音乐 */
  281. const playMusic = async () => {
  282. const playState = await togglePlay("play");
  283. // 取消播放,停止播放
  284. if (!playState) {
  285. evaluatingData.startBegin = false;
  286. handleCancelEvaluat();
  287. return;
  288. }
  289. // 检测播放进度, 计算延迟
  290. check_currentTime();
  291. // 如果开启了摄像头, 开启录制视频
  292. if (state.setting.camera) {
  293. console.log("开始录制视频");
  294. api_startCapture();
  295. }
  296. };
  297. let _audio: HTMLAudioElement;
  298. /** 录音开始,记录开始时间点 */
  299. const recordStartTimePoint = async (res?: IPostMessage) => {
  300. console.error("开始录音");
  301. // 没有开始评测,不处理
  302. if (!evaluatingData.startBegin) return;
  303. let inteveral = res?.content?.inteveral || 0;
  304. if (browserInfo.ios) {
  305. inteveral *= 1000;
  306. }
  307. evaluatingData.backtime = inteveral || Date.now();
  308. console.log(
  309. "🚀 ~ 开始时间点:",
  310. evaluatingData.backtime,
  311. "已经录的时间:",
  312. Date.now() - inteveral,
  313. "记录时间点:",
  314. Date.now()
  315. );
  316. // 是否禁播
  317. if (evaluatingData.isDisabledPlayMusic) {
  318. return;
  319. }
  320. // 开始播放
  321. playMusic();
  322. };
  323. /**
  324. * 结束评测
  325. * @param isComplete 是否完整评测
  326. * @returns
  327. */
  328. export const handleEndEvaluat = (isComplete = false) => {
  329. // 没有开始评测 , 不是评测模式 , 不评分
  330. if (!evaluatingData.startBegin || state.modeType !== "evaluating") return;
  331. evaluatingData.startBegin = false;
  332. // 结束录音
  333. api_stopRecording();
  334. // 结束评测
  335. endEvaluating({
  336. musicScoreId: state.examSongId,
  337. });
  338. showLoadingToast({
  339. message: "评分中",
  340. duration: 0,
  341. overlay: true,
  342. overlayClass: styles.scoreMode,
  343. });
  344. evaluatingData.isComplete = isComplete;
  345. // 如果开启了摄像头, 结束录制视频
  346. if (state.setting.camera) {
  347. console.log("结束录制视频");
  348. api_endCapture();
  349. }
  350. };
  351. /**
  352. * 结束评测
  353. */
  354. export const handleEndBegin = () => {
  355. handleEndEvaluat();
  356. handleStopPlay();
  357. };
  358. /**
  359. * 取消评测
  360. */
  361. export const handleCancelEvaluat = () => {
  362. evaluatingData.evaluatings = {};
  363. evaluatingData.startBegin = false;
  364. // 关闭提示
  365. closeToast();
  366. // 取消记录
  367. api_proxyServiceMessage({
  368. header: {
  369. commond: "recordCancel",
  370. type: "SOUND_COMPARE",
  371. status: 200,
  372. },
  373. });
  374. // 取消评测
  375. cancelEvaluating();
  376. // 停止播放
  377. handleStopPlay();
  378. };
  379. /** 查看报告 */
  380. export const handleViewReport = (
  381. key: "recordId" | "recordIdStr",
  382. type: "gym" | "colexiu" | "orchestra" | "instrument"
  383. ) => {
  384. const id = evaluatingData.resultData?.[key] || "";
  385. let url = "";
  386. switch (type) {
  387. case "gym":
  388. url = location.origin + location.pathname + "#/report/" + id;
  389. break;
  390. case "orchestra":
  391. url = location.origin + location.pathname + "report-share.html?id=" + id;
  392. break;
  393. case "instrument":
  394. url = location.origin + location.pathname + "#/evaluat-report?id=" + id;
  395. break;
  396. default:
  397. url = location.origin + location.pathname + "report-share.html?id=" + id;
  398. break;
  399. }
  400. api_openWebView({
  401. url,
  402. orientation: 0,
  403. isHideTitle: true, // 此处兼容安卓,意思为隐藏全部头部
  404. statusBarTextColor: false,
  405. isOpenLight: true,
  406. c_orientation: 0,
  407. });
  408. };
  409. // 隐藏存演奏按钮
  410. const handleComplexButton = (res?: IPostMessage) => {
  411. console.log('监听是否隐藏保存按钮', res)
  412. if (res?.content) {
  413. const { header, body } = res.content;
  414. state.isHideEvaluatReportSaveBtn = true
  415. }
  416. };
  417. export default defineComponent({
  418. name: "evaluating",
  419. setup() {
  420. const pageVisibility = usePageVisibility();
  421. // 需要记录的数据
  422. const record_old_data = reactive({
  423. /** 指法 */
  424. finger: false,
  425. /** 原音伴奏 */
  426. play_mode: "" as IPlayState,
  427. /** 评测是否要伴奏 */
  428. enableAccompaniment: true,
  429. });
  430. /** 记录状态 */
  431. const hanlde_record = () => {
  432. // 取消指法
  433. record_old_data.finger = state.setting.displayFingering;
  434. state.setting.displayFingering = false;
  435. // 切换为伴奏
  436. record_old_data.play_mode = state.playSource;
  437. record_old_data.enableAccompaniment = state.setting.enableAccompaniment;
  438. // 如果关闭伴奏,评测静音
  439. if (!record_old_data.enableAccompaniment) {
  440. console.log("关闭伴奏");
  441. toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : "background", true);
  442. }
  443. };
  444. /** 还原状态 */
  445. const handle_reduction = () => {
  446. // 还原指法
  447. state.setting.displayFingering = record_old_data.finger;
  448. state.playSource = record_old_data.play_mode;
  449. // 如果关闭伴奏, 结束评测取消静音
  450. if (!record_old_data.enableAccompaniment) {
  451. toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : "background", false);
  452. }
  453. };
  454. watch(pageVisibility, (value) => {
  455. if (value == "hidden" && evaluatingData.startBegin) {
  456. handleEndBegin();
  457. }
  458. });
  459. onMounted(() => {
  460. resetPlaybackToStart();
  461. hanlde_record();
  462. evaluatingData.resultData = {};
  463. // evaluatingData.resulstMode = true;
  464. // evaluatingData.resultData = {...getLeveByScore(10), score: 10, intonation: 10, cadence: 30, integrity: 40}
  465. // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData)
  466. evaluatingData.evaluatings = {};
  467. evaluatingData.soundEffectFrequency = 0;
  468. evaluatingData.checkStep = 0;
  469. evaluatingData.rendered = true;
  470. sendResult(handleScoreResult);
  471. hideComplexButton(handleComplexButton, true);
  472. api_recordStartTime(recordStartTimePoint);
  473. // 不是选段模式评测, 就清空已选段
  474. if (!state.isSelectMeasureMode) {
  475. clearSelection();
  476. }
  477. console.log("加载评测模块成功");
  478. });
  479. onUnmounted(() => {
  480. evaluatingData.checkEnd = false;
  481. evaluatingData.rendered = false;
  482. resetPlaybackToStart();
  483. removeResult(handleScoreResult);
  484. hideComplexButton(() => {}, false);
  485. api_remove_recordStartTime(recordStartTimePoint);
  486. handle_reduction();
  487. console.log("卸载评测模块成功");
  488. });
  489. return () => <div></div>;
  490. },
  491. });