123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- 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 audioBga1 from "../images/leftCloud.json";
- import audioBga2 from "../images/rightCloud.json";
- import videobg from "../images/videobg.png";
- import backImg from "../images/back.png";
- import {
- postMessage
- } from '@/helpers/native-message';
- import { usePageVisibility } from '@vant/use';
- import playProgressData from "./playProgress"
- import Loading from '../loading';
- import { NoticeBar } from "vant"
- import { generateMixedData } from "../audioVisualDraw"
- 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 videoBgUrl = decodeURIComponent(route.query.videoBgUrl as string || '');
- const playType = resourceUrl.lastIndexOf('mp4') !== -1 ? 'Video' : 'Audio'
- const landscapeScreen = ref(false)
- let _plrl:any
- const playIngShow = ref(true)
- const loaded = ref(false)
- let isInitAudioVisualDraw = false
- const { registerDrag, unRegisterDrag } = landscapeScreenDrag()
- const creationHeight = ref(0)
- 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<HTMLIFrameElement>()
- const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()
- function initPlay(){
- const id = playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";
- _plrl = new Plyr(id, {
- controls: ["play", "progress", "current-time", "duration"],
- fullscreen: {
- enabled: false,
- fallback: false
- }
- });
- // 在微信中运行的时候,微信没有开放自动加载资源的权限,所以要等播放之后才显示播放控制器
- _plrl.on('loadedmetadata', () => {
- loaded.value = true
- _plrl.currentTime = playProgressData.playProgress
- });
- _plrl.on('play', () => {
- playIngShow.value = false
- playStaff()
- });
- _plrl.on('pause', () => {
- playIngShow.value = true
- pauseStaff()
- });
- _plrl.on('ended', () => {
- _plrl.currentTime = 0
- if(!_plrl.playing){
- setTimeout(() => {
- updateProgressStaff(_plrl.currentTime)
- }, 100);
- }
- });
- _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(generateMixedData(48), canvasCtx, {
- // lineGap: 2,
- // canvWidth: width,
- // canvHeight: height,
- // canvFillColor: "transparent",
- // lineColor: "rgba(255, 255, 255, 0.7)"
- // })
- // if (!isPause) {
- // requestAnimationFrameFun()
- // }
- // })
- const _time = setInterval(() => {
- if (isPause) {
- clearInterval(_time)
- return
- }
- //analyser?.getByteFrequencyData(dataArray)
- draw(generateMixedData(38), canvasCtx, {
- lineGap: 3,
- canvWidth: width,
- canvHeight: height,
- canvFillColor: "transparent",
- lineColor: "rgba(28,172,241, 0.58)"
- })
- }, 300);
- }
- let isPause = true
- const playVisualDraw = () => {
- //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了
- isPause = false
- requestAnimationFrameFun()
- }
- const pauseVisualDraw = () => {
- isPause = true
- requestAnimationFrame(()=>{
- canvasCtx.clearRect(0, 0, width, height);
- })
- //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了
- // source?.disconnect()
- // analyser?.disconnect()
- }
- return {
- playVisualDraw,
- pauseVisualDraw
- }
- }
- function handlerBack(event:MouseEvent){
- event.stopPropagation()
- playProgressData.playState = !!_plrl?.playing
- playProgressData.playProgress = _plrl?.currentTime || 0
- 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', () => {
- playVisualDraw()
- });
- _plrl.on('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()
- setFullHeight()
- window.addEventListener('resize', updateLandscapeScreenState)
- window.addEventListener('resize', setFullHeight)
- }
- }
- 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 = `http://192.168.3.122:3000/instrument.html#/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(playProgressData.playState){
- handlerClickPlay()
- }else{
- updateProgressStaff(_plrl.currentTime)
- }
- }
- });
- }
- 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
- }
- }
- function setFullHeight(){
- // ios 浏览器100vh 不等于可视区
- requestAnimationFrame(()=>{
- creationHeight.value = window.innerHeight
- })
- }
- onMounted(()=>{
- // 五线谱
- initStaff()
- initPlay()
- handlerLandscapeScreen()
- })
- onUnmounted(()=>{
- if(isApp){
- postMessage({
- api: "setRequestedOrientation",
- content: {
- orientation: 1,
- },
- });
- }else{
- window.removeEventListener('resize', updateLandscapeScreenState)
- window.removeEventListener('resize', setFullHeight)
- }
- _plrl?.destroy()
- })
- return () =>
- <div id="landscapeScreenPlay"
- class={[styles.playCreation,landscapeScreen.value && styles.landscapeScreen,!loaded.value && styles.notLoaded, browser().isTablet && styles.ipadPlayCreation]}
- style={
- {
- "--creationHeight":creationHeight.value ? creationHeight.value+"px" : "100vh"
- }
- }
- onClick={
- (event)=>{
- staffState.isShow && handlerClickPlay(event)
- }
- }
- >
- <div class={styles.backBox}>
- <img class={styles.backImg} src={backImg} onClick={handlerBack} />
- <div class={styles.musicDetail}>
- <div class={styles.musicSheetName}>
- <NoticeBar
- text={musicSheetName}
- background="none"
- />
- </div>
- <div class={styles.username}>演奏:{username}</div>
- </div>
- </div>
- {
- playType === 'Audio' ?
- <div class={styles.audioBox}>
- <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
- <Vue3Lottie class={styles.audioBga} animationData={audioBga} autoPlay={true} loop={true}></Vue3Lottie>
- <Vue3Lottie class={styles.audioBga1} animationData={audioBga1} autoPlay={true} loop={true}></Vue3Lottie>
- <Vue3Lottie class={styles.audioBga2} animationData={audioBga2} autoPlay={true} 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={ videoBgUrl || videobg}
- preload="metadata"
- playsinline
- webkit-playsinline
- />
- }
- <div class={[styles.playLarge, playIngShow.value && styles.playIngShow]}></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>
- }
- {
- !staffState.isShow && <Loading></Loading>
- }
- </div>;
- }
- });
|