|
@@ -0,0 +1,217 @@
|
|
|
|
+import { defineComponent, onMounted, reactive, ref, onUnmounted } from 'vue';
|
|
|
|
+import { useRoute, useRouter } from 'vue-router';
|
|
|
|
+import { browser } 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';
|
|
|
|
+
|
|
|
|
+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 playType = resourceUrl.lastIndexOf('mp4') !== -1 ? 'Video' : 'Audio'
|
|
|
|
+ const lottieDom = ref()
|
|
|
|
+ const landscapeScreen = ref(false)
|
|
|
|
+ function initPlay(){
|
|
|
|
+ const id = playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";
|
|
|
|
+ const _plrl = new Plyr(id, {
|
|
|
|
+ controls: ["play-large", "play", "progress", "current-time", "duration"]
|
|
|
|
+ });
|
|
|
|
+ // 创建音波数据
|
|
|
|
+ if(playType === "Audio"){
|
|
|
|
+ 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()
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ /**
|
|
|
|
+ * 音频可视化
|
|
|
|
+ * @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")!
|
|
|
|
+ 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 = () => {
|
|
|
|
+ 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 = () => {
|
|
|
|
+ isPause = false
|
|
|
|
+ audioCtx.resume()
|
|
|
|
+ requestAnimationFrameFun()
|
|
|
|
+ }
|
|
|
|
+ const pauseVisualDraw = () => {
|
|
|
|
+ isPause = true
|
|
|
|
+ audioCtx.suspend()
|
|
|
|
+ }
|
|
|
|
+ return {
|
|
|
|
+ playVisualDraw,
|
|
|
|
+ pauseVisualDraw
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ function handlerBack(){
|
|
|
|
+ router.back()
|
|
|
|
+ }
|
|
|
|
+ function handlerLandscapeScreen(){
|
|
|
|
+ // app端调用app的横屏
|
|
|
|
+ if(isApp){
|
|
|
|
+ postMessage({
|
|
|
|
+ api: "setRequestedOrientation",
|
|
|
|
+ content: {
|
|
|
|
+ orientation: 0,
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+ }else{
|
|
|
|
+ // web端使用旋转的方式
|
|
|
|
+ updateLandscapeScreenState()
|
|
|
|
+ window.addEventListener('resize', updateLandscapeScreenState)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ function updateLandscapeScreenState(){
|
|
|
|
+ if(window.innerWidth > window.innerHeight){
|
|
|
|
+ landscapeScreen.value = false
|
|
|
|
+ }else{
|
|
|
|
+ landscapeScreen.value = true
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ handlerLandscapeScreen()
|
|
|
|
+ onMounted(()=>{
|
|
|
|
+ initPlay()
|
|
|
|
+ })
|
|
|
|
+ onUnmounted(()=>{
|
|
|
|
+ if(isApp){
|
|
|
|
+ postMessage({
|
|
|
|
+ api: "setRequestedOrientation",
|
|
|
|
+ content: {
|
|
|
|
+ orientation: 1,
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+ }else{
|
|
|
|
+ window.removeEventListener('resize', updateLandscapeScreenState)
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ return () =>
|
|
|
|
+ <div class={[styles.playCreation,landscapeScreen.value && styles.landscapeScreen]}>
|
|
|
|
+ <div class={styles.backBox}>
|
|
|
|
+ <img class={styles.backImg} src={backImg}onClick={handlerBack} />
|
|
|
|
+ <div class={styles.musicDetail}>
|
|
|
|
+ <div class={styles.musicSheetName}>{musicSheetName}</div>
|
|
|
|
+ <div class={styles.username}>{username}</div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ {
|
|
|
|
+ playType === 'Audio' ?
|
|
|
|
+ <div class={styles.audioBox}>
|
|
|
|
+ <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
|
|
|
|
+ <Vue3Lottie ref={lottieDom} class={styles.audioBga} animationData={audioBga} autoPlay={false} loop={true}></Vue3Lottie>
|
|
|
|
+ <audio
|
|
|
|
+ crossorigin="anonymous"
|
|
|
|
+ id="audioMediaSrc"
|
|
|
|
+ src={resourceUrl}
|
|
|
|
+ controls="false"
|
|
|
|
+ preload="metadata"
|
|
|
|
+ playsinline
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+ :
|
|
|
|
+ <video
|
|
|
|
+ id="videoMediaSrc"
|
|
|
|
+ class={styles.videoBox}
|
|
|
|
+ src={resourceUrl}
|
|
|
|
+ data-poster={videobg}
|
|
|
|
+ preload="metadata"
|
|
|
|
+ playsinline
|
|
|
|
+ />
|
|
|
|
+ }
|
|
|
|
+ </div>;
|
|
|
|
+ }
|
|
|
|
+});
|