index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import { defineComponent, onMounted, reactive, ref, onUnmounted, watch } from 'vue';
  2. import { useRoute, useRouter } from 'vue-router';
  3. import { browser, vaildMusicScoreUrl } from "@/helpers/utils"
  4. import styles from './index.module.less';
  5. import "plyr/dist/plyr.css";
  6. import Plyr from "plyr";
  7. import { Vue3Lottie } from "vue3-lottie";
  8. import audioBga from "../images/audioBga.json";
  9. import audioBga1 from "../images/leftCloud.json";
  10. import audioBga2 from "../images/rightCloud.json";
  11. import videobg from "../images/videobg.png";
  12. import backImg from "../images/back.png";
  13. import {
  14. postMessage
  15. } from '@/helpers/native-message';
  16. import { usePageVisibility } from '@vant/use';
  17. export default defineComponent({
  18. name: 'playCreation',
  19. setup() {
  20. const {isApp} = browser()
  21. const route = useRoute();
  22. const router = useRouter();
  23. const resourceUrl = decodeURIComponent(route.query.resourceUrl as string || '');
  24. const musicSheetName = decodeURIComponent(route.query.musicSheetName as string || '');
  25. const username = decodeURIComponent(route.query.username as string || '');
  26. const musicSheetId = decodeURIComponent(route.query.musicSheetId as string || '');
  27. const videoBgUrl = decodeURIComponent(route.query.videoBgUrl as string || '');
  28. const playType = resourceUrl.lastIndexOf('mp4') !== -1 ? 'Video' : 'Audio'
  29. const lottieDom = ref()
  30. const lottieDom1 = ref()
  31. const lottieDom2 = ref()
  32. const landscapeScreen = ref(false)
  33. let _plrl:any
  34. const playIngShow = ref(true)
  35. const loaded = ref(false)
  36. let isInitAudioVisualDraw = false
  37. const { registerDrag, unRegisterDrag } = landscapeScreenDrag()
  38. watch(landscapeScreen, ()=>{
  39. if(landscapeScreen.value){
  40. registerDrag()
  41. }else{
  42. unRegisterDrag()
  43. }
  44. })
  45. // 谱面
  46. const staffState = reactive({
  47. staffSrc: "",
  48. isShow: false,
  49. height:"initial",
  50. speedRate:Number(decodeURIComponent(route.query.speedRate as string || "1")),
  51. musicRenderType:decodeURIComponent(route.query.musicRenderType as string || "staff"),
  52. partIndex:Number(decodeURIComponent(route.query.partIndex as string || "0"))
  53. })
  54. const staffDom= ref<HTMLIFrameElement>()
  55. const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()
  56. function initPlay(){
  57. const id = playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";
  58. _plrl = new Plyr(id, {
  59. controls: ["play", "progress", "current-time", "duration"]
  60. });
  61. // 在微信中运行的时候,微信没有开放自动加载资源的权限,所以要等播放之后才显示播放控制器
  62. _plrl.on('loadedmetadata', () => {
  63. loaded.value = true
  64. });
  65. _plrl.on('play', () => {
  66. playIngShow.value = false
  67. playStaff()
  68. });
  69. _plrl.on('pause', () => {
  70. playIngShow.value = true
  71. pauseStaff()
  72. });
  73. _plrl.on('seeked', () => {
  74. // 暂停的时候调用
  75. if(!_plrl.playing){
  76. updateProgressStaff(_plrl.currentTime)
  77. }
  78. });
  79. }
  80. // 注册 横屏时候的自定义拖动事件 解决旋转时候拖动进度条坐标的问题
  81. function landscapeScreenDrag() {
  82. let progressBar: HTMLElement
  83. function onDown(e: MouseEvent | TouchEvent) {
  84. e.preventDefault();
  85. e.stopPropagation();
  86. // 记录拖动之前的状态
  87. const playing = _plrl.playing
  88. _plrl.pause()
  89. const isTouchEv = isTouchEvent(e);
  90. const event = isTouchEv ? e.touches[0] : e;
  91. setProgress(event)
  92. function onUp() {
  93. document.removeEventListener(
  94. isTouchEv ? 'touchmove' : 'mousemove',
  95. onMove
  96. );
  97. document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  98. playing && _plrl.play() // 之前是播放状态 就播放
  99. }
  100. function onMove(e: MouseEvent | TouchEvent){
  101. e.preventDefault();
  102. e.stopPropagation();
  103. const event = isTouchEvent(e) ? e.touches[0] : e;
  104. setProgress(event)
  105. }
  106. function setProgress(event: MouseEvent | Touch) {
  107. const {top, height} = progressBar.getBoundingClientRect();
  108. const progress = (event.clientY - top) / height;
  109. const progressNum = Math.min(Math.max(progress, 0), 1);
  110. _plrl.currentTime = progressNum * _plrl.duration
  111. }
  112. document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove);
  113. document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  114. }
  115. function registerDrag() {
  116. if(!progressBar){
  117. progressBar = document.querySelector('#landscapeScreenPlay .plyr__progress__container') as HTMLElement;
  118. }
  119. progressBar.addEventListener('mousedown', onDown);
  120. progressBar.addEventListener('touchstart', onDown);
  121. }
  122. function unRegisterDrag() {
  123. progressBar.removeEventListener('mousedown', onDown);
  124. progressBar.removeEventListener('touchstart', onDown);
  125. }
  126. return {
  127. registerDrag,
  128. unRegisterDrag
  129. }
  130. }
  131. function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
  132. return window.TouchEvent && e instanceof window.TouchEvent;
  133. }
  134. /**
  135. * 音频可视化
  136. * @param audioDom
  137. * @param canvasDom
  138. * @param fftSize 2的幂数,最小为32
  139. */
  140. function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
  141. type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
  142. // canvas
  143. const canvasCtx = canvasDom.getContext("2d")!
  144. let { width, height } = canvasDom.getBoundingClientRect()
  145. // 这里横屏的时候需要旋转,所以宽高变化一下
  146. if(landscapeScreen.value){
  147. const _width = width
  148. width = height
  149. height = _width
  150. }
  151. canvasDom.width = width
  152. canvasDom.height = height
  153. // audio
  154. const audioCtx = new AudioContext()
  155. const source = audioCtx.createMediaElementSource(audioDom)
  156. const analyser = audioCtx.createAnalyser()
  157. analyser.fftSize = fftSize
  158. source?.connect(analyser)
  159. analyser.connect(audioCtx.destination)
  160. const dataArray = new Uint8Array(fftSize / 2)
  161. const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
  162. if (!ctx) return
  163. const w = canvWidth
  164. const h = canvHeight
  165. fillCanvasBackground(ctx, w, h, canvFillColor)
  166. // 可视化
  167. const dataLen = data.length
  168. let step = (w / 2 - lineGap * dataLen) / dataLen
  169. step < 1 && (step = 1)
  170. const midX = w / 2
  171. const midY = h / 2
  172. let xLeft = midX
  173. for (let i = 0; i < dataLen; i++) {
  174. const value = data[i]
  175. const percent = value / 255 // 最大值为255
  176. const barHeight = percent * midY
  177. canvasCtx.fillStyle = lineColor
  178. // 中间加间隙
  179. if (i === 0) {
  180. xLeft -= lineGap / 2
  181. }
  182. canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
  183. canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
  184. xLeft -= step + lineGap
  185. }
  186. let xRight = midX
  187. for (let i = 0; i < dataLen; i++) {
  188. const value = data[i]
  189. const percent = value / 255 // 最大值为255
  190. const barHeight = percent * midY
  191. canvasCtx.fillStyle = lineColor
  192. if (i === 0) {
  193. xRight += lineGap / 2
  194. }
  195. canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
  196. canvasCtx.fillRect(xRight, midY, step, barHeight)
  197. xRight += step + lineGap
  198. }
  199. }
  200. const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
  201. ctx.clearRect(0, 0, w, h)
  202. ctx.fillStyle = colors
  203. ctx.fillRect(0, 0, w, h)
  204. }
  205. const requestAnimationFrameFun = () => {
  206. requestAnimationFrame(() => {
  207. analyser?.getByteFrequencyData(dataArray)
  208. draw(dataArray, canvasCtx, {
  209. lineGap: 2,
  210. canvWidth: width,
  211. canvHeight: height,
  212. canvFillColor: "transparent",
  213. lineColor: "rgba(255, 255, 255, 0.7)"
  214. })
  215. if (!isPause) {
  216. requestAnimationFrameFun()
  217. }
  218. })
  219. }
  220. let isPause = true
  221. const playVisualDraw = () => {
  222. //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了
  223. isPause = false
  224. requestAnimationFrameFun()
  225. }
  226. const pauseVisualDraw = () => {
  227. isPause = true
  228. //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了
  229. // source?.disconnect()
  230. // analyser?.disconnect()
  231. }
  232. return {
  233. playVisualDraw,
  234. pauseVisualDraw
  235. }
  236. }
  237. function handlerBack(event:MouseEvent){
  238. event.stopPropagation()
  239. router.back()
  240. }
  241. //点击改变播放状态
  242. function handlerClickPlay(event:MouseEvent){
  243. //由于ios低版本必须在用户操作之后才能初始化 createMediaElementSource 所以必须在用户操作之后初始化
  244. if(!isInitAudioVisualDraw && playType === "Audio"){
  245. isInitAudioVisualDraw = true
  246. // 创建音波数据
  247. const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement
  248. const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
  249. const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
  250. _plrl.on('play', () => {
  251. lottieDom.value.play()
  252. lottieDom1.value.play()
  253. lottieDom2.value.play()
  254. playVisualDraw()
  255. });
  256. _plrl.on('pause', () => {
  257. lottieDom.value.pause()
  258. lottieDom1.value.pause()
  259. lottieDom2.value.pause()
  260. pauseVisualDraw()
  261. });
  262. }
  263. // 原生 播放暂停按钮 点击的时候 不触发
  264. // @ts-ignore
  265. if(event.target?.matches('button.plyr__control')){
  266. return
  267. }
  268. if (_plrl.playing) {
  269. _plrl.pause();
  270. } else {
  271. _plrl.play();
  272. }
  273. }
  274. function handlerLandscapeScreen(){
  275. // app端调用app的横屏
  276. if(isApp){
  277. postMessage({
  278. api: "setRequestedOrientation",
  279. content: {
  280. orientation: 0,
  281. },
  282. });
  283. }else{
  284. // web端使用旋转的方式
  285. updateLandscapeScreenState()
  286. window.addEventListener('resize', updateLandscapeScreenState)
  287. }
  288. }
  289. function updateLandscapeScreenState(){
  290. // 下一帧 计算 横竖屏切换太快 获取的宽高不准
  291. requestAnimationFrame(()=>{
  292. if(window.innerWidth > window.innerHeight){
  293. landscapeScreen.value = false
  294. }else{
  295. landscapeScreen.value = true
  296. }
  297. })
  298. }
  299. const pageVisibility = usePageVisibility();
  300. watch(pageVisibility, value => {
  301. if (value === 'hidden') {
  302. _plrl?.pause();
  303. }
  304. });
  305. // 初始化五线谱
  306. function initStaff(){
  307. const src = `${vaildMusicScoreUrl()}/instrument/#/simple-detail?id=${musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
  308. //const src = `http://192.168.3.122:3000/instrument.html#/simple-detail?id=${musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
  309. staffState.staffSrc = src
  310. window.addEventListener('message', (event) => {
  311. const { api, height } = event.data;
  312. if (api === 'api_musicPage') {
  313. staffState.isShow = true
  314. staffState.height = height + "px"
  315. // 如果是播放中自动开始 播放
  316. if(_plrl.playing){
  317. playStaff()
  318. }
  319. }
  320. });
  321. }
  322. function staffMoveInstance(){
  323. let isPause = true
  324. const requestAnimationFrameFun = () => {
  325. requestAnimationFrame(() => {
  326. staffDom.value?.contentWindow?.postMessage(
  327. {
  328. api: 'api_playProgress',
  329. content: {
  330. currentTime: _plrl.currentTime * staffState.speedRate
  331. }
  332. },
  333. "*"
  334. )
  335. if (!isPause) {
  336. requestAnimationFrameFun()
  337. }
  338. })
  339. }
  340. const playStaff = () => {
  341. // 没渲染不执行
  342. if(!staffState.isShow) return
  343. isPause = false
  344. staffDom.value?.contentWindow?.postMessage(
  345. {
  346. api: 'api_play'
  347. },
  348. "*"
  349. )
  350. requestAnimationFrameFun()
  351. }
  352. const pauseStaff = () => {
  353. // 没渲染不执行
  354. if(!staffState.isShow) return
  355. isPause = true
  356. staffDom.value?.contentWindow?.postMessage(
  357. {
  358. api: 'api_paused'
  359. },
  360. "*"
  361. )
  362. }
  363. const updateProgressStaff = (currentTime: number) => {
  364. // 没渲染不执行
  365. if(!staffState.isShow) return
  366. staffDom.value?.contentWindow?.postMessage(
  367. {
  368. api: 'api_updateProgress',
  369. content: {
  370. currentTime: currentTime * staffState.speedRate
  371. }
  372. },
  373. "*"
  374. )
  375. }
  376. return {
  377. playStaff,
  378. pauseStaff,
  379. updateProgressStaff
  380. }
  381. }
  382. onMounted(()=>{
  383. // 五线谱
  384. initStaff()
  385. initPlay()
  386. handlerLandscapeScreen()
  387. })
  388. onUnmounted(()=>{
  389. if(isApp){
  390. postMessage({
  391. api: "setRequestedOrientation",
  392. content: {
  393. orientation: 1,
  394. },
  395. });
  396. }else{
  397. window.removeEventListener('resize', updateLandscapeScreenState)
  398. }
  399. })
  400. return () =>
  401. <div id="landscapeScreenPlay" class={[styles.playCreation,landscapeScreen.value && styles.landscapeScreen,!loaded.value && styles.notLoaded]} onClick={handlerClickPlay}>
  402. <div class={styles.backBox}>
  403. <img class={styles.backImg} src={backImg} onClick={handlerBack} />
  404. <div class={styles.musicDetail}>
  405. <div class={styles.musicSheetName}>{musicSheetName}</div>
  406. <div class={styles.username}>{username}</div>
  407. </div>
  408. </div>
  409. {
  410. playType === 'Audio' ?
  411. <div class={styles.audioBox}>
  412. <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
  413. <Vue3Lottie ref={lottieDom} class={styles.audioBga} animationData={audioBga} autoPlay={false} loop={true}></Vue3Lottie>
  414. <Vue3Lottie ref={lottieDom1} class={styles.audioBga1} animationData={audioBga1} autoPlay={false} loop={true}></Vue3Lottie>
  415. <Vue3Lottie ref={lottieDom2} class={styles.audioBga2} animationData={audioBga2} autoPlay={false} loop={true}></Vue3Lottie>
  416. <audio
  417. crossorigin="anonymous"
  418. id="audioMediaSrc"
  419. src={resourceUrl}
  420. controls="false"
  421. preload="metadata"
  422. playsinline
  423. />
  424. </div>
  425. :
  426. <video
  427. id="videoMediaSrc"
  428. class={styles.videoBox}
  429. src={resourceUrl}
  430. data-poster={ videoBgUrl || videobg}
  431. preload="metadata"
  432. playsinline
  433. />
  434. }
  435. <div class={[styles.playLarge, playIngShow.value && styles.playIngShow]}></div>
  436. {/* 谱面 */}
  437. {
  438. staffState.staffSrc &&
  439. <div
  440. class={[styles.staffBox, staffState.isShow && styles.staffBoxShow]}
  441. style={
  442. {
  443. '--staffBoxHeight':staffState.height
  444. }
  445. }
  446. >
  447. <div class={styles.mask}></div>
  448. <iframe
  449. ref={staffDom}
  450. class={styles.staff}
  451. frameborder="0"
  452. src={staffState.staffSrc}>
  453. </iframe>
  454. </div>
  455. }
  456. </div>;
  457. }
  458. });