vr_player/vr/src/components/VRLive/index.vue
2025-02-08 00:55:28 +08:00

852 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="videoContainer" class="vr-video">
<div class="video-wrapper">
<div id="video" @mousemove="ControlVisible()">
</div>
<div v-if="playVariables.statistics" class="vr-statistics">
<p>type{{playVariables.type || '???'}}</p>
<p>status{{playVariables.status || '???'}}</p>
<p>currentTime: {{playVariables.currentTime}}</p>
<p>totalTime: {{playVariables.totalTime}}</p>
<span v-if="deviceOrientationData.isSupported">
<p>
陀螺仪数据:
</p>
<p>
alpha(z轴):{{deviceOrientationData.alpha||'手机查看参数'}}
</p>
<p>
beta(x轴):{{deviceOrientationData.beta||'手机查看参数'}}
</p>
<p>
gamma(y轴):{{deviceOrientationData.gamma||'手机查看参数'}}
</p></span>
</div>
<div class="vr-func">
<div v-if="playVariables.status == 'pause'&&!playVariables.playClick"
@click="player.play();playVariables.playClick=true" class="btns-play">
<i class="iconfont icon-icon_play"></i>
</div>
<div v-else-if="playVariables.status == 'playing'&&!playVariables.playClick" @click="player.pause()"
class="btns-pause">
<i class="iconfont icon-ai07"></i>
</div>
</div>
<div v-if="playVariables.status == 'loading'" class="vr-loading">
<div class="border"></div>
<div class="slow"></div>
<div class="fast"></div>
</div>
<div class="vr-bar" v-if="playVariables.playClick" id="control">
<div class="bg"></div>
<div class="btns">
<div v-if="playVariables.status === 'playing'" @click="player.pause()" class="btns-pause">
<i class="iconfont icon-ai07"></i>
</div>
<div v-else @click="player.play()" class="btns-play">
<i class="iconfont icon-icon_play"></i>
</div>
</div>
<div v-if="playVariables.type == 'normal'" class="progress-container">
<div @click="jumpTo($event)" class="progress-wrapper">
<div class="progress" id="progress-play"></div>
</div>
<div class="btn-wrapper">
<div class="btn" id="progress-btn"></div>
</div>
</div>
<div v-else class="type">
<span class="statu-circle"></span>
<span>{{playVariables.type}}</span>
</div>
<div @click="changeFullscreenStatu()" class="fullscreen">
<i class="iconfont icon-quanping"></i>
</div>
</div>
<div class="vr-notice" id="vrNotice" v-html="playVariables.notice">
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import threeOrbitControls from 'three-orbit-controls'
import flvjs from 'flv.js'
const OrbitControls = threeOrbitControls(THREE)
export default {
name: 'VRLive',
props: {
option: {
default: Object
}
},
data () {
return {
camera: null,
scene: null,
renderer: null,
mesh: null,
video: null,
controls: null,
deviceOrientationData: {},
hls: null,
player: {},
videoContainer: '',
playVariables: {
/*
视频类型默认Normal
Normal
HLS
FLV
*/
type: 'Normal',
/*
播放状态,与视频播放状态对应
loading:加载中,
playing:视频播放中,包括视频中间加载后继续播放
pause:暂停或用户未点开始按钮
*/
status: 'pause',
playClick: false,
// 控件显示状态
currentTime: 0,
progress: 0,
// fullscreenStatu: false,
notice: '',
statistics: false,
error: {
code: 0,
msg: ''
}
}
// cameraVariables: {
// isUserInteracting: false,
// lon: 0,
// lat: 0,
// phi: 0,
// theta: 0,
// distance: 1,
// onPointerDownPointerX: 0,
// onPointerDownPointerY: 0,
// onPointerDownLon: 0,
// onPointerDownLat: 0,
// sensitivity: 0.1
// }
}
},
// computed: {
// fullScreenStatu () {
// return document.mozFullScreen || document.webkitIsFullScreen
// }
// },
// watch: {
// playVariables (val) {
// },
// // fullScreenStatu (val) {
// // console.log(val)
// // if (val) {
// // this.videoContainer.classList.add('fullScreen-mobile')
// // } else {
// // this.videoContainer.classList.remove('fullScreen-mobile')
// // }
// // }
// },
mounted () {
if (this.check()) {
this.playVariables.statistics = this.option.statistics
this.playVariables.type = this.option.source.type
this.videoContainer = document.getElementById('videoContainer')
this.init()
}
// 检查浏览器音频支持
if (!flvjs.isSupported()) {
console.error('Browser does not support flv.js')
return
}
// 检查浏览器音频编解码器支持
const audioTest = document.createElement('audio')
if (!audioTest.canPlayType('audio/aac')) {
console.warn('Browser might not support AAC audio codec')
}
},
methods: {
check () {
if (!this.option.source) {
console.log(new Error('无播放源'))
} else if (!this.option.source.type) {
console.log(new Error('播放源type不能为空'))
} else if (!this.option.source.url) {
console.log(new Error('播放源url不能为空'))
} else {
return true
}
},
init () {
const container = document.getElementById('video')
this.initScene()
this.initCamera(container)
this.initRenderer(container)
this.initContent()
this.initControls(container)
this.render()
// this.addMouseEvent(container)
window.addEventListener('deviceorientation', function (event) {
this.deviceOrientationData.isSupported = true
this.deviceOrientationData = event
}.bind(this), false)
window.addEventListener('resize', function () {
this.onWindowResize(container)
}.bind(this))
},
initScene () {
this.scene = new THREE.Scene()
},
initCamera (el) {
this.camera = new THREE.PerspectiveCamera(75, el.clientWidth / el.clientHeight, 1, 1100)
this.camera.position.set(1, 0, 0)
// this.camera.target = new THREE.Vector3(0, 0, 0)
},
initRenderer (el) {
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(el.offsetWidth, el.offsetHeight)
el.appendChild(this.renderer.domElement)
},
initVideo () {
this.video = document.createElement('video')
this.video.preload = 'auto'
this.video.muted = false
this.video.volume = 1.0
this.video.crossOrigin = 'anonymous'
// 立即设置 player 为 video 元素
this.player = this.video
this.video.addEventListener('waiting', function (event) {
this.playVariables.status = 'loading'
}.bind(this))
this.video.addEventListener('playing', function (event) {
this.playVariables.status = 'playing'
}.bind(this))
this.video.addEventListener('pause', function (event) {
this.playVariables.status = 'pause'
}.bind(this))
this.video.addEventListener('canplay', function (event) {
this.playVariables.duration = this.player.duration
if (this.playVariables.status === 'loading') {
this.playVariables.status = 'playing'
}
}.bind(this))
// 判断视频类型
switch (this.option.source.type) {
case 'flv':
this.getFLV()
break
case 'hls':
this.getHLS(this.option.source.url, this.video)
break
case 'normal':
this.getNormalVideo(this.option.source.url, this.video)
break
default:
this.playVariables.error.code = 1
this.playVariables.error.msg = '未知的视频类型'
break
}
},
initContent () {
this.initVideo()
var geometry = new THREE.SphereBufferGeometry(300, 90, 90)
geometry.scale(-1, 1, 1)
var texture = new THREE.VideoTexture(this.video)
texture.minFilter = THREE.LinearFilter
texture.format = THREE.RGBFormat
var material = new THREE.MeshBasicMaterial({
map: texture
})
this.mesh = new THREE.Mesh(geometry, material)
this.mesh.position.set(0, 0, 0)
this.scene.add(this.mesh)
},
initControls (el) {
this.controls = new OrbitControls(this.camera, el)
// this.controls.target = new THREE.Vector3(0, Math.PI, 0)
this.controls.rotateSpeed = 0.05
this.controls.enableDamping = true
this.controls.dampingFactor = 0.05
},
render () {
requestAnimationFrame(this.render)
this.controls.update()
// this.cameraUpdate()
this.renderer.render(this.scene, this.camera)
},
getNormalVideo (sourceURL, el) {
const source = document.createElement('source')
source.src = sourceURL
// source.type = 'video/mp4'
el.appendChild(source)
this.player = el
},
getHLS (sourceURL, el) {
const Hls = require('hls.js')
if (Hls.isSupported()) {
this.hls = new Hls()
this.hls.loadSource(sourceURL)
this.hls.attachMedia(el)
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('加载成功')
})
this.hls.on(Hls.Events.ERROR, (event, data) => {
throw new Error(data.response.code + ' ' + data.response.text)
})
}
this.player = el
},
// abandoned 对接rtmp流
// 非原生video标签。
// confuse object?如何作为纹理渲染?
// getRTMP (sourceURL, el) {
// el.id = 'rtmpVideo'
// console.log(el)
// this.player = videojs(el, {
// sources: [{
// src: sourceURL
// }]
// })
// this.player.on('ready', function () {
// console.log('准备就绪')
// })
// },
getFLV () {
if (flvjs.isSupported()) {
const flvPlayer = flvjs.createPlayer({
type: 'flv',
hasAudio: true,
hasVideo: true,
url: this.option.source.url
})
// 添加事件监听器
flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
console.error('FLV Player Error:', errorType, errorDetail)
})
flvPlayer.on(flvjs.Events.MEDIA_INFO, (mediaInfo) => {
console.log('FLV Media Info:', mediaInfo)
})
flvPlayer.attachMediaElement(this.video)
flvPlayer.load()
return flvPlayer
}
},
onWindowResize (el) {
this.camera.aspect = el.clientWidth / el.clientHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(el.clientWidth, el.clientHeight)
},
// jumpTo ($e) {
// console.log(e)
// }
async changeFullscreenStatu () {
if (this.playVariables.fullscreenStatu) {
this.exitFullscreen()
// screen.orientation.unlock()
// this.videoContainer.classList.remove('full-screen-mobile')
} else {
this.fullScreen()
if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)) {
this.showNotice('为了更好的观看体验<br>请关闭屏幕锁定横屏观看该视频')
}
// 只在谷歌浏览器下生效
// screen.orientation.lock('landscape-primary')
// this.videoContainer.classList.add('full-screen-mobile')
// screen.lockOrientationUniversal = screen.lockOrientation || screen.mozLockOrientation || screen.msLockOrientation
// new ScreenOrientation().lock('landscape-secondary')
// console.log(screen)
// screen.msLockOrientation.lock('landscape-primary')
}
this.playVariables.fullscreenStatu = !this.playVariables.fullscreenStatu
},
fullScreen () {
const el = this.videoContainer
if (el.requestFullscreen) {
el.requestFullscreen()
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
} else if (el.webkitRequestFullScreen) {
el.webkitRequestFullScreen()
}
},
exitFullscreen () {
const el = document
el.exitFullscreen()
if (el.exitFullscreen) {
el.exitFullscreen()
} else if (el.mozCancelFullScreen) {
el.mozCancelFullScreen()
} else if (el.webkitCancelFullScreen) {
el.webkitCancelFullScreen()
}
},
showNotice (msg) {
this.playVariables.notice = msg
const notice = document.getElementById('vrNotice')
if (notice.classList.contains('vr-notice-show')) {
notice.classList.remove('vr-notice-show')
setTimeout(() => {
notice.classList.add('vr-notice-show')
}, 0)
} else {
notice.classList.add('vr-notice-show')
}
},
ControlVisible () {
const control = document.getElementById('control')
if (this.playVariables.status === 'playing') {
if (control.classList.contains('control-hidden')) {
control.classList.remove('control-hidden')
setTimeout(() => {
control.classList.add('control-hidden')
}, 0)
} else {
control.classList.add('control-hidden')
}
}
}
// 有更好的选择 three轨道控制器
// abandoned 对接鼠标移动事件,剩余惯性尚需对接,使滑动体验更加流畅
// addMouseEvent (el) {
// el.addEventListener('mousedown', onDocumentMouseDown.bind(this), false)
// el.addEventListener('mousemove', onDocumentMouseMove.bind(this), false)
// el.addEventListener('mouseup', onDocumentMouseUp.bind(this), false)
// el.addEventListener('touchstart', onDocumentTouchStart.bind(this), false)
// el.addEventListener('touchmove', onDocumentTouchMove.bind(this), false)
// el.addEventListener('touchend', onDocumentTouchEnd.bind(this), false)
// function onDocumentMouseDown (event) {
// event.preventDefault()
// this.cameraVariables.isUserInteracting = true
// this.cameraVariables.onPointerDownPointerX = event.clientX
// this.cameraVariables.onPointerDownPointerY = event.clientY
// this.cameraVariables.onPointerDownLon = this.cameraVariables.lon
// this.cameraVariables.onPointerDownLat = this.cameraVariables.lat
// }
// function onDocumentMouseMove (event) {
// if (this.cameraVariables.isUserInteracting === true) {
// this.cameraVariables.lon =
// (this.cameraVariables.onPointerDownPointerX - event.clientX) * this.cameraVariables.sensitivity +
// this.cameraVariables.onPointerDownLon
// this.cameraVariables.lat =
// (event.clientY - this.cameraVariables.onPointerDownPointerY) * this.cameraVariables.sensitivity +
// this.cameraVariables.onPointerDownLat
// console.log(this.cameraVariables.lat)
// }
// }
// function onDocumentMouseUp () {
// this.cameraVariables.isUserInteracting = false
// }
// function onDocumentTouchStart (event) {
// event.preventDefault()
// this.cameraVariables.isUserInteracting = true
// this.cameraVariables.onPointerDownPointerX = event.touches[0].clientX
// this.cameraVariables.onPointerDownPointerY = event.touches[0].clientY
// this.cameraVariables.onPointerDownLon = this.cameraVariables.lon
// this.cameraVariables.onPointerDownLat = this.cameraVariables.lat
// }
// function onDocumentTouchMove (event) {
// if (this.cameraVariables.isUserInteracting === true) {
// this.cameraVariables.lon =
// (this.cameraVariables.onPointerDownPointerX - event.touches[0].clientX) * this.cameraVariables
// .sensitivity +
// this.cameraVariables.onPointerDownLon
// this.cameraVariables.lat =
// (event.touches[0].clientY - this.cameraVariables.onPointerDownPointerY) * this.cameraVariables
// .sensitivity +
// this.cameraVariables.onPointerDownLat
// }
// }
// function onDocumentTouchEnd () {
// this.cameraVariables.isUserInteracting = false
// }
// // const pre = {
// // x: '',
// // y: ''
// // }
// // // const cur = {
// // // x: '',
// // // y: ''
// // // }
// // let isDown = false
// // const self = this
// // el.onmousedown = function (event) {
// // isDown = true
// // }
// // el.onmouseup = function (event) {
// // isDown = false
// // }
// // el.onmousemove = function (event) {
// // if (isDown) {
// // console.log(event)
// // console.log(pre)
// // console.log(self.camera)
// // if (event.movementY && !event.movementX) {
// // self.camera.rotation.x -= event.movementY / 500
// // } else if (!event.movementY && event.movementX) {
// // self.camera.rotation.y += event.movementX / 500
// // } else {
// // pre.x = event.clientX
// // }
// // pre.y = event.clientY
// // }
// // }
// },
// cameraUpdate () {
// this.cameraVariables.lat = Math.max(-85, Math.min(85, this.cameraVariables.lat))
// this.cameraVariables.phi = THREE.Math.degToRad(90 - this.cameraVariables.lat)
// this.cameraVariables.theta = THREE.Math.degToRad(this.cameraVariables.lon)
// this.camera.position.x = this.cameraVariables.distance * Math.sin(this.cameraVariables.phi) * Math.cos(this.cameraVariables.theta)
// this.camera.position.y = this.cameraVariables.distance * Math.cos(this.cameraVariables.phi)
// this.camera.position.z = this.cameraVariables.distance * Math.sin(this.cameraVariables.phi) * Math.sin(this.cameraVariables.theta)
// this.camera.lookAt(this.camera.target)
// // 为后面的惯性对接
// setTimeout(() => {
// }, 1000)
// }
}
}
</script>
<style lang="scss" scoped>
.vr-video {
position: relative;
width: 100%;
height: 100%;
.video-wrapper {
display: flex;
align-items: center;
height: 100%;
// min-height: 300px;
}
#video {
width: 100%;
height: 100%;
&:hover {
.control {
display: block;
animation: mousehover 3s;
animation-delay: 5s;
animation-fill-mode: forwards;
}
}
}
.control-hidden {
visibility: visible;
opacity: 1;
animation: mousehover 2s;
animation-delay: 3s;
animation-fill-mode: forwards;
}
@keyframes mousehover {
100% {
visibility: hidden;
opacity: 0;
}
}
.vr {
&-func {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 50px;
font-size: 16px;
.btns-play,
.btns-pause {
i {
font-size: 4em;
color: rgba(255, 255, 255, 1);
transition: color .3s;
}
&:hover {
cursor: pointer;
i {
color: rgba(255, 255, 255, 0.6);
}
}
}
}
&-loading {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
width: 3em;
height: 3em;
.border {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0.45em solid rgba(99, 149, 168, 0.5);
border-radius: 50%;
}
.slow {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-width: 0.45em;
border-style: solid;
border: 0.45em solid rgba(0, 0, 0, 0);
border-top-color: rgb(7, 186, 241);
animation: slow 1.5s linear infinite;
border-radius: 50%;
}
.fast {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-width: 0.45em;
border-style: solid;
border: 0.45em solid rgba(0, 0, 0, 0);
border-top-color: rgb(10, 126, 161);
animation: fast 0.75s linear infinite;
border-radius: 50%;
}
@keyframes slow {
to {
transform: rotate(360deg);
}
}
@keyframes fast {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(45deg);
}
50% {
border-top-color: rgb(195, 236, 248);
}
75% {
transform: rotate(315deg);
}
100% {
transform: rotate(360deg);
}
}
}
&-bar {
position: absolute;
z-index: 10;
bottom: 0;
display: flex;
align-items: center;
padding: 1em 1em;
box-sizing: border-box;
width: 100%;
background-image: linear-gradient(to top, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
.btns {
padding: 0.5em;
&-play,
&-pause {
i {
color: white;
}
}
&:hover {
cursor: pointer;
}
}
.progress-container {
position: relative;
flex-grow: 1;
display: flex;
align-items: center;
padding: 0 1em;
.progress-wrapper {
position: relative;
width: 100%;
height: 0.2em;
border-radius: 30px;
background-color: rgba(150, 150, 150, 1);
overflow: hidden;
.progress {
position: absolute;
width: 0%;
height: 100%;
background-color: rgb(22, 175, 236);
transition: width .5s;
}
&:hover {
cursor: pointer;
}
}
.btn-wrapper {
position: absolute;
z-index: 9;
left: 1em;
width: calc(100% - 2em);
.btn {
margin-left: 0;
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: white;
}
}
}
.type {
padding: 0 2em;
width: 100%;
font-size: 0.95em;
font-weight: 300;
color: white;
.statu-circle {}
}
.fullscreen {
i {
font-size: 1.4em;
color: white;
}
&:hover {
cursor: pointer;
}
}
}
&-notice {
position: absolute;
z-index: 99;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
visibility: hidden;
opacity: 0;
padding: 0.5em 1em;
white-space: nowrap;
font-size: 1em;
color: white;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 0.5em;
&-show {
visibility: visible;
opacity: 1;
animation: notice 2s;
animation-delay: 3s;
animation-fill-mode: forwards;
}
@keyframes notice {
100% {
visibility: hidden;
opacity: 0;
}
}
}
&-statistics {
position: absolute;
z-index: 1;
top: 1em;
left: 1em;
padding: 0.2em 1em;
font-size: 13px;
color: white;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 0.2em;
}
}
&:fullscreen {
.video-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
// transform-origin: top left;
// transform: rotate(90deg) translate(-0vh, -100vw);
}
}
}
// .full-screen-mobile {
// .video-wrapper {
// position: absolute;
// z-index: 99;
// top: 0;
// left: 0;
// width: 100vh;
// height: 100vw;
// transform-origin: top left;
// transform: rotate(90deg) translate(-0vh, -100vw);
// }
// // #video {
// // width: 100%;
// // height: calc(100vw / 16 * 9);
// // }
// }
</style>