好久没更新了,写一下我半年前写的vr播放器把,那时候的项目接到一个需求,需要实现VR视频播放,并能够播放直播。直播我熟,但是这个VR播放就涉及到我的知识盲区了。
于是开始开始研究threejs及相关方面的知识,在一段时间的调研后大致有了一些头绪,大概就是生成一个球的模型,摄像头放在中间,使用轨迹库实现模型方向调转,然后纹理朝向内部,最后把视频作为纹理加载到这个模型上就行了。虽然说起来很简单,但在实现的过程种还是踩了不少坑的。

一开始准备用我比较熟悉的vue-video-player来作纹理,结果发现里面需要使用原生video dom节点作为纹理,当时想着那这样就没什么办法了,要么去修改这个库,看看能不能插进去作为一个插件使用,想了想还是自己写一个播放器,这样的话有很多杂七杂八的东西像是进度条什么的都只能自己去实现了,然后实现hls,rtmp等直播流的播放,比较麻烦。关于rtmp流的播放的话,并不是通过h5video标签实现播放的,而是用flash实现播放的,标签也不是h5video标签,而是object标签,感觉就算了,google浏览器都宣布12月底就停止flash的支持了,那基于flash的rtmp直播协议也没有太大对接的必要了。

直播流协议介绍

目前主流的直播流协议有以下三种,其他的像是webrtc也是比较主流的做法,但是它会更偏向于音视频通话,基于它客户端一对一建立连接通道传输音频这种网状连接机制,也不太适合大规模多人直播,如果要做多人直播,就要搭建webrtc服务器做转发,这种原先我也有实现的意愿,理论上也是可以实现的,但是实现起来也比较麻烦,也没有这方面的需求,就没有做。
RTMP: 实时消息传输协议,由Adobe公司研发,但当前还没有收入国际标准(wikipedia)。协议比较全能,既可以用来推送又可以用来直播,其核心理念是将大块的视频帧和音频帧“剁碎”,然后以小数据包的形式在互联网上进行传输,且支持加密,因此隐私性相对比较理想,但拆包组包的过程比较复杂,所以在海量并发时容易出现一些不可预期的稳定性问题。曾经在市场上有相当多直播采用该协议,但由于flash的淘汰,已渐渐被hls,跟flv取代。
HLS :基于HTTP协议的流直播(wikipedia)。苹果推出的解决方案,将视频分成 5-10 秒的视频小分片,然后用 m3u8 索引表进行管理。由于客户端下载到的视频都是 5-10 秒的完整数据,故视频的流畅性很好,但也同样引入了很大的延迟(HLS 的一般延迟在 10-30s 左右)。相比于 FLV, HLS 在iPhone 和大部分 Android 手机浏览器上的支持非常给力,所以常用于 QQ 和微信朋友圈的URL 分享。
HTTP-FLV :由 Adobe 公司主推,格式极其简单,只是在大块的视频帧和音视频头部加入一些标记头信息,由于这种极致的简洁,在延迟表现和大规模并发方面都很成熟。唯一的不足就是在手机浏览器上的支持非常有限,但是用作手机端 APP 直播协议却异常合适。直播流详解

以下是具体实现。

一.安装threejs,flv.js,hls.js 模块

threejs:WebGL三维引擎库,如果不知道这是什么的话还是建议去了解一下的,网上文档介绍的应该比我详细,这里我就不做介绍了。相关文档:threejs电子书官方文档
hlsjs:hls直播流相关库
flvjs:flv直播流相关库

npm i three -S
npm install hls.js --save
npm install flv.js --save
二.搭建场景

为了能够让场景借助three.js来进行显示,需要以下几个对象:场景、相机和渲染器,这样就能透过摄像机渲染出场景。

1.创建scene

创建场景实例

initScene () {
      this.scene = new THREE.Scene()
}
2.创建camera

创建相机实例,相当于眼睛,相机看到的就是呈现在画布上的东西

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)
}
3.创建Render

创建渲染器,用于将我们创建好的实例渲染到画布上

initRenderer (el) {
      this.renderer = new THREE.WebGLRenderer()
      this.renderer.setSize(el.offsetWidth, el.offsetHeight)
      el.appendChild(this.renderer.domElement)
}
4.创建video

创建video标签,接受普通全景视频,或全景直播流,后面将作为纹理渲染到球模型上,里面有些东西都是我在写控件时加上去的,懒得分离出去再写一遍了😂

initVideo () {
      this.video = document.createElement('video')
      this.video.preload = 'auto'
      this.video.muted = true
      this.video.crossOrigin = 'anonymous'
      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))
      if (this.playVariables.type === 'normal') {
        this.video.ontimeupdate = function (event) {
          this.playVariables.currentTime = Math.floor(this.player.currentTime)
          this.playVariables.totalTime = this.playVariables.totalTime ? this.playVariables.totalTime : Math.floor(
            this.player.duration)
          this.playVariables.progress = this.playVariables.currentTime / this.playVariables.totalTime
          document.getElementById('progress-play').style.width = (this.playVariables.progress) * 100 + '%'
          document.getElementById('progress-btn').style.marginLeft = (this.playVariables.progress) * 100 + '%'
        }.bind(this)
      // 判断视频类型
      switch (this.option.source.type) {
        case 'flv':
          this.getFLV(this.option.source.url, this.video)
          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
      }
}
5.创建球

创建模型,并将video作为其纹理

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)
}
5.创建轨道控制器

这个是我们实现视角控制的关键,当我们点击鼠标拖动的时候,其实是在操作摄像机的位置,在转动的是我们摄像机位置,整个场景的直角坐标系是没有在变动的,如果以场景做参照物,那么就相当于相机在围绕直角坐标系原点转动,而摄像机视角始终面向原点,从而实现鼠标拖动视角转动。
创建之后,我设置了转动速度大小,开启转动惯性,设置了转动惯性大小,具体自己注释掉试一试就知道了。其他更多属性设置就自己看下文档

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
}
6.渲染循环

让我们的画布上的东西动起来就需要创建一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒),threejs为我们提供了requestAnimationFrame函数来控制渲染,也可以把requestAnimationFrame看作是定时器,定时执行自身。this.controls.update()更新轨道控制器,this.renderer.render则是调用渲染器渲染。

render () {
      requestAnimationFrame(this.render)
      this.controls.update()
      // this.cameraUpdate()
      this.renderer.render(this.scene, this.camera)
    }
7.为普通视频,直播流配置不同属性的函数

让我们的画布上的东西动起来就需要创建一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒),threejs为我们提供了requestAnimationFrame函数来控制渲染,也可以把requestAnimationFrame看作是定时器,定时执行自身。this.controls.update()更新轨道控制器,this.renderer.render则是调用渲染器渲染。

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
}
8.编写init函数统一调用

让我们的画布上的东西动起来就需要创建一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒),threejs为我们提供了requestAnimationFrame函数来控制渲染,也可以把requestAnimationFrame看作是定时器,定时执行自身。this.controls.update()更新轨道控制器,this.renderer.render则是调用渲染器渲染。

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))
}
9.在生命周期mounted中调用init函数

因为涉及到dom操作,所以在vue实例挂载后调用init函数

mounted () {
    if (this.check()) {
      this.playVariables.statistics = this.option.statistics
      this.playVariables.type = this.option.source.type
      this.videoContainer = document.getElementById('videoContainer')
      this.init()
    }
}

总结:现在感觉写这个真的踩了太多坑了,因为当时写这个运行环境是在小程序,本身wx小程序对threejs这些webgl方面的东西就不怎么支持,根据教程引入的threejs模块也是阉割版,然后dom操作也是阉割的,写播放器控件都写的很难受,最后是放在webview上实现的,然后android跟ios又有很多区别,像是ios只支持hls直播协议,flv直播流无法播放,android则两者都支持之类的问题一大堆,调试也很麻烦,webview都没有报错信息,很多时候出问题都不知道哪里出问题,解决也不知道怎么解决,写这个小程序时候血压都拉满了,web端就没这么多的事情,开发体验直接拉满,写下来就很流畅,麻了😅。

目前这个只是实现了基本的播放,真要写的话其实还有许多需要补充的,不过事情太多了,应该是没空写这个了。代码的话已上传至仓库vue-vr

Logo

前往低代码交流专区

更多推荐