import { defineComponent, onMounted, reactive, ref, onUnmounted, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { browser, vaildMusicScoreUrl } from "@/helpers/utils" import styles from './index.module.less'; import "plyr/dist/plyr.css"; import Plyr from "plyr"; import { Vue3Lottie } from "vue3-lottie"; import audioBga from "../images/audioBga.json"; import videobg from "../images/videobg.png"; import backImg from "../images/back.png"; import { postMessage } from '@/helpers/native-message'; import { usePageVisibility } from '@vant/use'; export default defineComponent({ name: 'playCreation', setup() { const {isApp} = browser() const route = useRoute(); const router = useRouter(); const resourceUrl = decodeURIComponent(route.query.resourceUrl as string || ''); const musicSheetName = decodeURIComponent(route.query.musicSheetName as string || ''); const username = decodeURIComponent(route.query.username as string || ''); const musicSheetId = decodeURIComponent(route.query.musicSheetId as string || ''); const playType = resourceUrl.lastIndexOf('mp4') !== -1 ? 'Video' : 'Audio' const lottieDom = ref() const landscapeScreen = ref(false) let _plrl:any const playIngShow = ref(true) const loaded = ref(false) let isInitAudioVisualDraw = false const { registerDrag, unRegisterDrag } = landscapeScreenDrag() watch(landscapeScreen, ()=>{ if(landscapeScreen.value){ registerDrag() }else{ unRegisterDrag() } }) // 谱面 const staffState = reactive({ staffSrc: "", isShow: false, height:"initial", speedRate:Number(decodeURIComponent(route.query.speedRate as string || "1")), musicRenderType:decodeURIComponent(route.query.musicRenderType as string || "staff"), partIndex:Number(decodeURIComponent(route.query.partIndex as string || "0")) }) const staffDom= ref() const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance() function initPlay(){ const id = playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc"; _plrl = new Plyr(id, { controls: ["play", "progress", "current-time", "duration"] }); // 在微信中运行的时候,微信没有开放自动加载资源的权限,所以要等播放之后才显示播放控制器 _plrl.on('loadedmetadata', () => { loaded.value = true }); _plrl.on('play', () => { playIngShow.value = false playStaff() }); _plrl.on('pause', () => { playIngShow.value = true pauseStaff() }); _plrl.on('seeked', () => { // 暂停的时候调用 if(!_plrl.playing){ updateProgressStaff(_plrl.currentTime) } }); } // 注册 横屏时候的自定义拖动事件 解决旋转时候拖动进度条坐标的问题 function landscapeScreenDrag() { let progressBar: HTMLElement function onDown(e: MouseEvent | TouchEvent) { e.preventDefault(); e.stopPropagation(); // 记录拖动之前的状态 const playing = _plrl.playing _plrl.pause() const isTouchEv = isTouchEvent(e); const event = isTouchEv ? e.touches[0] : e; setProgress(event) function onUp() { document.removeEventListener( isTouchEv ? 'touchmove' : 'mousemove', onMove ); document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp); playing && _plrl.play() // 之前是播放状态 就播放 } function onMove(e: MouseEvent | TouchEvent){ e.preventDefault(); e.stopPropagation(); const event = isTouchEvent(e) ? e.touches[0] : e; setProgress(event) } function setProgress(event: MouseEvent | Touch) { const {top, height} = progressBar.getBoundingClientRect(); const progress = (event.clientY - top) / height; const progressNum = Math.min(Math.max(progress, 0), 1); _plrl.currentTime = progressNum * _plrl.duration } document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove); document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp); } function registerDrag() { if(!progressBar){ progressBar = document.querySelector('#landscapeScreenPlay .plyr__progress__container') as HTMLElement; } progressBar.addEventListener('mousedown', onDown); progressBar.addEventListener('touchstart', onDown); } function unRegisterDrag() { progressBar.removeEventListener('mousedown', onDown); progressBar.removeEventListener('touchstart', onDown); } return { registerDrag, unRegisterDrag } } function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent { return window.TouchEvent && e instanceof window.TouchEvent; } /** * 音频可视化 * @param audioDom * @param canvasDom * @param fftSize 2的幂数,最小为32 */ function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) { type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number } // canvas const canvasCtx = canvasDom.getContext("2d")! let { width, height } = canvasDom.getBoundingClientRect() // 这里横屏的时候需要旋转,所以宽高变化一下 if(landscapeScreen.value){ const _width = width width = height height = _width } canvasDom.width = width canvasDom.height = height // audio const audioCtx = new AudioContext() const source = audioCtx.createMediaElementSource(audioDom) const analyser = audioCtx.createAnalyser() analyser.fftSize = fftSize source?.connect(analyser) analyser.connect(audioCtx.destination) const dataArray = new Uint8Array(fftSize / 2) const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => { if (!ctx) return const w = canvWidth const h = canvHeight fillCanvasBackground(ctx, w, h, canvFillColor) // 可视化 const dataLen = data.length let step = (w / 2 - lineGap * dataLen) / dataLen step < 1 && (step = 1) const midX = w / 2 const midY = h / 2 let xLeft = midX for (let i = 0; i < dataLen; i++) { const value = data[i] const percent = value / 255 // 最大值为255 const barHeight = percent * midY canvasCtx.fillStyle = lineColor // 中间加间隙 if (i === 0) { xLeft -= lineGap / 2 } canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight) canvasCtx.fillRect(xLeft - step, midY, step, barHeight) xLeft -= step + lineGap } let xRight = midX for (let i = 0; i < dataLen; i++) { const value = data[i] const percent = value / 255 // 最大值为255 const barHeight = percent * midY canvasCtx.fillStyle = lineColor if (i === 0) { xRight += lineGap / 2 } canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight) canvasCtx.fillRect(xRight, midY, step, barHeight) xRight += step + lineGap } } const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => { ctx.clearRect(0, 0, w, h) ctx.fillStyle = colors ctx.fillRect(0, 0, w, h) } const requestAnimationFrameFun = () => { requestAnimationFrame(() => { analyser?.getByteFrequencyData(dataArray) draw(dataArray, canvasCtx, { lineGap: 2, canvWidth: width, canvHeight: height, canvFillColor: "transparent", lineColor: "rgba(255, 255, 255, 0.7)" }) if (!isPause) { requestAnimationFrameFun() } }) } let isPause = true const playVisualDraw = () => { //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了 isPause = false requestAnimationFrameFun() } const pauseVisualDraw = () => { isPause = true //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了 // source?.disconnect() // analyser?.disconnect() } return { playVisualDraw, pauseVisualDraw } } function handlerBack(event:MouseEvent){ event.stopPropagation() router.back() } //点击改变播放状态 function handlerClickPlay(event:MouseEvent){ //由于ios低版本必须在用户操作之后才能初始化 createMediaElementSource 所以必须在用户操作之后初始化 if(!isInitAudioVisualDraw && playType === "Audio"){ isInitAudioVisualDraw = true // 创建音波数据 const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom) _plrl.on('play', () => { lottieDom.value.play() playVisualDraw() }); _plrl.on('pause', () => { lottieDom.value.pause() pauseVisualDraw() }); } // 原生 播放暂停按钮 点击的时候 不触发 // @ts-ignore if(event.target?.matches('button.plyr__control')){ return } if (_plrl.playing) { _plrl.pause(); } else { _plrl.play(); } } function handlerLandscapeScreen(){ // app端调用app的横屏 if(isApp){ postMessage({ api: "setRequestedOrientation", content: { orientation: 0, }, }); }else{ // web端使用旋转的方式 updateLandscapeScreenState() window.addEventListener('resize', updateLandscapeScreenState) } } function updateLandscapeScreenState(){ // 下一帧 计算 横竖屏切换太快 获取的宽高不准 requestAnimationFrame(()=>{ if(window.innerWidth > window.innerHeight){ landscapeScreen.value = false }else{ landscapeScreen.value = true } }) } const pageVisibility = usePageVisibility(); watch(pageVisibility, value => { if (value === 'hidden') { _plrl?.pause(); } }); // 初始化五线谱 function initStaff(){ const src = `${vaildMusicScoreUrl()}/instrument/#/simple-detail?id=${musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`; //const src = `https://dev.kt.colexiu.com/instrument/#/simple-detail?id=${musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`; staffState.staffSrc = src window.addEventListener('message', (event) => { const { api, height } = event.data; if (api === 'api_musicPage') { staffState.isShow = true staffState.height = height + "px" // 如果是播放中自动开始 播放 if(_plrl.playing){ playStaff() } } }); } function staffMoveInstance(){ let isPause = true const requestAnimationFrameFun = () => { requestAnimationFrame(() => { staffDom.value?.contentWindow?.postMessage( { api: 'api_playProgress', content: { currentTime: _plrl.currentTime * staffState.speedRate } }, "*" ) if (!isPause) { requestAnimationFrameFun() } }) } const playStaff = () => { // 没渲染不执行 if(!staffState.isShow) return isPause = false staffDom.value?.contentWindow?.postMessage( { api: 'api_play' }, "*" ) requestAnimationFrameFun() } const pauseStaff = () => { // 没渲染不执行 if(!staffState.isShow) return isPause = true staffDom.value?.contentWindow?.postMessage( { api: 'api_paused' }, "*" ) } const updateProgressStaff = (currentTime: number) => { // 没渲染不执行 if(!staffState.isShow) return staffDom.value?.contentWindow?.postMessage( { api: 'api_updateProgress', content: { currentTime: currentTime * staffState.speedRate } }, "*" ) } return { playStaff, pauseStaff, updateProgressStaff } } onMounted(()=>{ // 五线谱 initStaff() initPlay() handlerLandscapeScreen() }) onUnmounted(()=>{ if(isApp){ postMessage({ api: "setRequestedOrientation", content: { orientation: 1, }, }); }else{ window.removeEventListener('resize', updateLandscapeScreenState) } }) return () =>
{musicSheetName}
{username}
{ playType === 'Audio' ?
:
; } });