index.tsx 12 KB

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