|
@@ -37,7 +37,7 @@ import iconZanActive from './images/icon-zan-active.png';
|
|
|
import iconPlay from './images/icon-play.png';
|
|
|
import iconPause from './images/icon-pause.png';
|
|
|
import { postMessage, promisefiyPostMessage } from '@/helpers/native-message';
|
|
|
-import { browser, getGradeCh, getSecondRPM } from '@/helpers/utils';
|
|
|
+import { browser, getGradeCh, getSecondRPM, vaildMusicScoreUrl } from '@/helpers/utils';
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
import {
|
|
|
api_userMusicDetail,
|
|
@@ -89,6 +89,14 @@ export default defineComponent({
|
|
|
mediaTimeShow: false,
|
|
|
playIngShow: true
|
|
|
})
|
|
|
+ // 谱面
|
|
|
+ const staffState = reactive({
|
|
|
+ staffSrc: "",
|
|
|
+ isShow: false,
|
|
|
+ height:"initial"
|
|
|
+ })
|
|
|
+ const staffDom= ref<HTMLIFrameElement>()
|
|
|
+ const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()
|
|
|
// 获取列表
|
|
|
const getStarList = async () => {
|
|
|
try {
|
|
@@ -179,28 +187,28 @@ export default defineComponent({
|
|
|
const player = state._plrl
|
|
|
// 创建音波数据
|
|
|
if(state.playType === "Audio"){
|
|
|
- setTimeout(() => {
|
|
|
- const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement
|
|
|
- const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
|
|
|
- const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
|
|
|
- player.on('play', () => {
|
|
|
- lottieDom.value.play()
|
|
|
- playVisualDraw()
|
|
|
- });
|
|
|
- player.on('pause', () => {
|
|
|
- lottieDom.value.pause()
|
|
|
- pauseVisualDraw()
|
|
|
- });
|
|
|
- }, 300); // 弹窗动画是0.25秒 这里用定时器 确保canvas 能获取到宽高
|
|
|
+ const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement
|
|
|
+ const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
|
|
|
+ const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
|
|
|
+ player.on('play', () => {
|
|
|
+ lottieDom.value.play()
|
|
|
+ playVisualDraw()
|
|
|
+ });
|
|
|
+ player.on('pause', () => {
|
|
|
+ lottieDom.value.pause()
|
|
|
+ pauseVisualDraw()
|
|
|
+ });
|
|
|
}
|
|
|
player.on("timeupdate", ()=>{
|
|
|
plyrState.currentTime = player.currentTime
|
|
|
})
|
|
|
player.on('play', () => {
|
|
|
plyrState.playIngShow = false
|
|
|
+ playStaff()
|
|
|
});
|
|
|
player.on('pause', () => {
|
|
|
plyrState.playIngShow = true
|
|
|
+ pauseStaff()
|
|
|
});
|
|
|
// 处理按压事件
|
|
|
const handleStart = () => {
|
|
@@ -210,6 +218,10 @@ export default defineComponent({
|
|
|
// 处理松开事件
|
|
|
const handleEnd = () => {
|
|
|
plyrState.mediaTimeShow = false
|
|
|
+ // 暂停的时候调用
|
|
|
+ if(!player.playing){
|
|
|
+ updateProgressStaff(player.currentTime)
|
|
|
+ }
|
|
|
};
|
|
|
const progressDom = document.querySelector("#playMediaSection .plyr__controls .plyr__progress__container") as HTMLElement
|
|
|
progressDom.addEventListener('mousedown', handleStart);
|
|
@@ -231,104 +243,190 @@ export default defineComponent({
|
|
|
* @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")!
|
|
|
- const { width, height } = canvasDom.getBoundingClientRect()
|
|
|
- canvasDom.width = width
|
|
|
- canvasDom.height = height
|
|
|
- // audio
|
|
|
- const audioCtx = new AudioContext()
|
|
|
- const analyser = audioCtx.createAnalyser()
|
|
|
- const source = audioCtx.createMediaElementSource(audioDom)
|
|
|
- analyser.fftSize = fftSize
|
|
|
- source.connect(analyser)
|
|
|
- analyser.connect(audioCtx.destination)
|
|
|
- const dataArray = new Uint8Array(analyser.frequencyBinCount)
|
|
|
- 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
|
|
|
- const step = (w / 2 - lineGap * dataLen) / dataLen
|
|
|
- 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 = () => {
|
|
|
+ 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")!
|
|
|
+ const { width, height } = canvasDom.getBoundingClientRect()
|
|
|
+ canvasDom.width = width
|
|
|
+ canvasDom.height = height
|
|
|
+ // audio
|
|
|
+ let audioCtx : AudioContext | null = null
|
|
|
+ let analyser : AnalyserNode | null = null
|
|
|
+ let source : MediaElementAudioSourceNode | null = null
|
|
|
+ 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 = () => {
|
|
|
+ if (!audioCtx) {
|
|
|
+ audioCtx = new AudioContext()
|
|
|
+ source = audioCtx.createMediaElementSource(audioDom)
|
|
|
+ analyser = audioCtx.createAnalyser()
|
|
|
+ analyser.fftSize = fftSize
|
|
|
+ source?.connect(analyser)
|
|
|
+ analyser.connect(audioCtx.destination)
|
|
|
+ }
|
|
|
+ //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了
|
|
|
+ isPause = false
|
|
|
+ requestAnimationFrameFun()
|
|
|
+ }
|
|
|
+ const pauseVisualDraw = () => {
|
|
|
+ isPause = true
|
|
|
+ //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了
|
|
|
+ // source?.disconnect()
|
|
|
+ // analyser?.disconnect()
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ playVisualDraw,
|
|
|
+ pauseVisualDraw
|
|
|
+ }
|
|
|
+ }
|
|
|
+ function handlerLandscapeScreen(event:any){
|
|
|
+ event.stopPropagation()
|
|
|
+ router.push({
|
|
|
+ path:"/playCreation",
|
|
|
+ query:{
|
|
|
+ resourceUrl:encodeURIComponent(state.musicDetail?.videoUrl),
|
|
|
+ musicSheetName:encodeURIComponent(state.musicDetail?.musicSheetName),
|
|
|
+ username:encodeURIComponent(state.musicDetail?.username),
|
|
|
+ musicSheetId:encodeURIComponent(state.musicDetail?.musicSheetId)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 初始化五线谱
|
|
|
+ function initStaff(){
|
|
|
+ //const src = `${vaildMusicScoreUrl()}/instrument/#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=staff`;
|
|
|
+ const src = `http://192.168.3.68:3000/instrument.html#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=staff`;
|
|
|
+ staffState.staffSrc = src
|
|
|
+ window.addEventListener('message', (event) => {
|
|
|
+ const { api, height } = event.data;
|
|
|
+ if (api === 'api_musicPage') {
|
|
|
+ staffState.isShow = true
|
|
|
+ staffState.height = height + "px"
|
|
|
+ // 如果是播放中自动开始 播放
|
|
|
+ if(state._plrl.playing){
|
|
|
+ playStaff()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ function staffMoveInstance(){
|
|
|
+ let isPause = true
|
|
|
+ const requestAnimationFrameFun = () => {
|
|
|
requestAnimationFrame(() => {
|
|
|
- analyser.getByteFrequencyData(dataArray)
|
|
|
- draw(dataArray, canvasCtx, {
|
|
|
- lineGap: 2,
|
|
|
- canvWidth: width,
|
|
|
- canvHeight: height,
|
|
|
- canvFillColor: "transparent",
|
|
|
- lineColor: "rgba(255, 255, 255, 0.7)"
|
|
|
- })
|
|
|
+ staffDom.value?.contentWindow?.postMessage(
|
|
|
+ {
|
|
|
+ api: 'api_playProgress',
|
|
|
+ content: {
|
|
|
+ currentTime: state._plrl.currentTime
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "*"
|
|
|
+ )
|
|
|
if (!isPause) {
|
|
|
requestAnimationFrameFun()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
- let isPause = true
|
|
|
- const playVisualDraw = () => {
|
|
|
+ const playStaff = () => {
|
|
|
+ // 没渲染不执行
|
|
|
+ if(!staffState.isShow) return
|
|
|
isPause = false
|
|
|
- audioCtx.resume()
|
|
|
+ staffDom.value?.contentWindow?.postMessage(
|
|
|
+ {
|
|
|
+ api: 'api_play'
|
|
|
+ },
|
|
|
+ "*"
|
|
|
+ )
|
|
|
requestAnimationFrameFun()
|
|
|
}
|
|
|
- const pauseVisualDraw = () => {
|
|
|
+ const pauseStaff = () => {
|
|
|
+ // 没渲染不执行
|
|
|
+ if(!staffState.isShow) return
|
|
|
isPause = true
|
|
|
- audioCtx.suspend()
|
|
|
+ staffDom.value?.contentWindow?.postMessage(
|
|
|
+ {
|
|
|
+ api: 'api_paused'
|
|
|
+ },
|
|
|
+ "*"
|
|
|
+ )
|
|
|
}
|
|
|
+ const updateProgressStaff = (currentTime: string) => {
|
|
|
+ // 没渲染不执行
|
|
|
+ if(!staffState.isShow) return
|
|
|
+ staffDom.value?.contentWindow?.postMessage(
|
|
|
+ {
|
|
|
+ api: 'api_updateProgress',
|
|
|
+ content: {
|
|
|
+ currentTime: state._plrl.currentTime
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "*"
|
|
|
+ )
|
|
|
+ }
|
|
|
return {
|
|
|
- playVisualDraw,
|
|
|
- pauseVisualDraw
|
|
|
+ playStaff,
|
|
|
+ pauseStaff,
|
|
|
+ updateProgressStaff
|
|
|
}
|
|
|
- }
|
|
|
- function handlerLandscapeScreen(event:any){
|
|
|
- event.stopPropagation()
|
|
|
- router.push({
|
|
|
- path:"/playCreation",
|
|
|
- query:{
|
|
|
- resourceUrl:encodeURIComponent(state.musicDetail?.videoUrl),
|
|
|
- musicSheetName:encodeURIComponent(state.musicDetail?.musicSheetName),
|
|
|
- username:encodeURIComponent(state.musicDetail?.username),
|
|
|
- }
|
|
|
- })
|
|
|
}
|
|
|
onMounted(async () => {
|
|
|
setStatusBarTextColor(true)
|
|
@@ -353,6 +451,8 @@ export default defineComponent({
|
|
|
return;
|
|
|
}
|
|
|
state.musicDetail = res.data || {};
|
|
|
+ // 五线谱
|
|
|
+ initStaff()
|
|
|
getStarList();
|
|
|
// 判断是视频还是音频
|
|
|
if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {
|
|
@@ -441,6 +541,26 @@ export default defineComponent({
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class={styles.landscapeScreen} onClick={handlerLandscapeScreen}></div>
|
|
|
+ {/* 谱面 */}
|
|
|
+ {
|
|
|
+ staffState.staffSrc &&
|
|
|
+ <div
|
|
|
+ class={[styles.staffBox, staffState.isShow && styles.staffBoxShow]}
|
|
|
+ style={
|
|
|
+ {
|
|
|
+ '--staffBoxHeight':staffState.height
|
|
|
+ }
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div class={styles.mask}></div>
|
|
|
+ <iframe
|
|
|
+ ref={staffDom}
|
|
|
+ class={styles.staff}
|
|
|
+ frameborder="0"
|
|
|
+ src={staffState.staffSrc}>
|
|
|
+ </iframe>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
</div>
|
|
|
</Sticky>
|
|
|
<div class={styles.musicSection}>
|