最近做了一个全景直播的小项目,走了一下大概的流程,特此记录一下。


1.直播服务器

首先利用srs搭建直播服务器,这部分我是跟着官方wiki走的,详细内容请移步

v4_CN_Home · ossrs/srs Wiki (github.com)https://github.com/ossrs/srs/wiki/v4_CN_Home#getting-started


2.全景相机与推流

全景相机我是用的insta360 x2,推流需要配合一台安卓设备,下载官网的应用软件,

全景相机,运动相机 - Insta360影石官网,360度全景运动相机https://www.insta360.com/cn/download/insta360-onex2安装好后将全景相机连接到手机上,有两种连接方法,一种是通过wifi连接,相机开一个热点,手机连接这个热点,然后推流的话走的是手机的流量。另一种方法是通过otg线连接,插上线后手机会弹出个提示框记得确认。

软件打开并连接上相机后按中间黄色的按钮然后右下角三个杠点开,选择直播模式,输入rtmp开头的推流地址,点击红色的点点就开始推流。


3.全景播放与拉流

*本项目基于vue3+ts

创建vue项目,安装所需依赖:

yarn add video.js videojs-contrib-hls @babylonjs/core @babylonjs/inspector @types/video.js

由于视频自动播放现在受到限制,要么静音自动播放,要么需要用户主动触发播放事件,并且在safari上陀螺仪需要用户主动去获取权限,所以我们需要一个加载组件LoadPage.vue,

<template><div><div class="permissionBg" v-show="showPerm"><div class="permissionContent"><div class="permissionText">为达到最佳用户体验<br>请给予陀螺仪权限</div><div class="permissionConfirm" @click="getGyroscope">好</div></div></div><div class="loadBg"><div class="loadButton" @click="logTest">点击开始</div></div></div>
</template>
<script setup lang="ts">
import { defineEmits, ref } from 'vue'
// emit
const emit = defineEmits(['start'])// 点击开始
const logTest = () => {emit('start') // 告诉父组件用户开始了
}
// 获取safari陀螺仪权限
const showPerm = ref(true)
const getGyroscope = () => {console.log(1)if (window.DeviceOrientationEvent) { // 如果存在DeviceOrientationEvent继续下面// eslint-disable-next-line @typescript-eslint/ban-ts-comment// @ts-ignoreif (typeof window.DeviceOrientationEvent.requestPermission === 'function') { // 如果DeviceOrientationEvent上存在requestPermission方法继续// eslint-disable-next-line @typescript-eslint/ban-ts-comment// @ts-ignorewindow.DeviceOrientationEvent.requestPermission() // 这玩应是safari独有的方法.then(permissionState => {if (permissionState === 'granted') {// handle datashowPerm.value = false} else {// handle denied}}).catch((err) => {console.log(err)})} else {showPerm.value = false}} else {showPerm.value = false}
}
</script>

根组件app.vue中引入LoadPage.vue,

<template>
<LoadPage @start="startLive" v-show="LoadPageShow"></LoadPage>
<canvas ref="babylon" class="canvas"></canvas>
</template><script setup lang="ts">
import { onMounted, ref } from 'vue'
import VrLive from './utils/vr-live'
import LoadPage from './components/LoadPage.vue'
// babylonCanvas
const babylonCanvas = ref()
// show LoadPage
const LoadPageShow = ref(true)
let live:VrLive
onMounted(() => {live = new VrLive(babylonCanvas.value, 'https://www.zhiyongw.com:5443/hls/abc.m3u8')
})
const startLive = () => {live.initBabylon()live.video.muted = false // 解除静音live.video.play()LoadPageShow.value = false // 隐藏loading页面
}
</script>

app.vue中导入的VrLive对象是我们核心功能类,构造函数需要接收一个canvas、一个直播拉流地址,然后需要公开生成的videoElement,(当然你也可以事先写好然后传进来)

项目src目录中新建utils文件夹,创建VrLive.ts到utils中

import videojs from 'video.js'
import 'videojs-contrib-hls'
import { Color4, Engine, Scene, DeviceOrientationCamera, Vector3, Mesh, CreateSphere, VideoTexture, StandardMaterial } from '@babylonjs/core'
import '@babylonjs/inspector'
/***vr直播组件,* @param canvas*        babylonjs运行所需要的canvas** @param videoSrc*        视频直播接流地址** @return vr直播组件的实例*/
export default class VrLive {private canvas!:HTMLCanvasElementprivate _video!: HTMLVideoElementprivate videoSrc!:stringprivate engine!:Engine // Engineprivate scene!:Scene // Sceneprivate camera!:DeviceOrientationCamera // cameraprivate videoBall!:Meshconstructor (canvas:HTMLCanvasElement, videoSrc:string) {if (canvas && videoSrc) {this.canvas = canvasthis.video = document.createElement('video')this.videoSrc = videoSrcthis.initVideo()}}// initBabylonpublic initBabylon () {this.initEngine(this.canvas)this.initVideoBall()this.initCamera()this.debuggerLayer()}// 初始化videoprivate initVideo () {this.creatVideoElement() // 创建视频标签if (!this.isIPhone()) {const vrVideo = videojs(this.video, // id或者videoElement{ }, // 配置项() => {vrVideo.play()} // 加载完成后的回调)}}// 创建视频标签private creatVideoElement () {// video标签this.video.id = 'vrVideo'this.video.muted = truethis.video.autoplay = truethis.video.src = this.videoSrc// videoSorce标签const videoSorce = document.createElement('source')videoSorce.type = 'application/x-mpegURL'videoSorce.src = this.videoSrcthis.video.appendChild(videoSorce)this.video.playsInline = truethis.video.style.width = '100vw'this.video.style.height = '10vh'this.video.style.position = 'absolute'this.video.style.top = '0'this.video.style.left = '0'this.video.style.zIndex = '20'// 在safari上,虚拟dom是没办法播放出声音的,所以我们需要吧这玩应怼进真实dom中然后用css给它藏起来document.getElementsByTagName('body')[0].appendChild(this.video)// 添加进domthis.video.style.display = 'none'}// isIPhoneprivate isIPhone ():boolean {const u = navigator.userAgentreturn !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) || u.indexOf('iPhone') > -1 || u.indexOf('iPad') > -1 // ios终端}// 初始化引擎private async initEngine (canvas: HTMLCanvasElement): Promise<void> {this.canvas = canvas // 初始化canvasthis.engine = new Engine(this.canvas,true,{},true)this.scene = new Scene(this.engine)// 初始化场景//   this.scene.clearColor = new Color4(0.784, 0.878, 1, 1)this.scene.clearColor = new Color4(1, 1, 1, 1)const assumedFramesPerSecond = 60const earthGravity = -9.34this.scene.gravity = new Vector3(0, earthGravity / assumedFramesPerSecond, 0) // 设置场景重力(模拟地球)this.scene.collisionsEnabled = true // 开启碰撞检测this.RenderLoop()// 执行渲染循环}// 渲染循环private RenderLoop ():void {// 添加窗口变化事件监听window.addEventListener('resize', () => {this.engine.resize()})// 执行循环this.engine.runRenderLoop(() => {this.scene.render()})}// debuggerLayer(Shift+Ctrl+Alt+I)private debuggerLayer ():void {window.addEventListener('keydown', (ev) => {if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) {if (this.scene.debugLayer.isVisible()) {this.scene.debugLayer.hide()} else {this.scene.debugLayer.show({ embedMode: true })}}})}// 初始化相机private initCamera ():void {this.camera = new DeviceOrientationCamera('vrLiveCamera',new Vector3(0, 0, 0),this.scene)// eslint-disable-next-line dot-notationthis.camera.inputs.remove(this.camera.inputs.attached['keyboard'])this.camera.attachControl(this.canvas, true)this.camera.fov = 1.5this.camera.fovMode = DeviceOrientationCamera.FOVMODE_HORIZONTAL_FIXED}// 初始化视频球private initVideoBall ():void {// meshthis.videoBall = CreateSphere('VideoBall',{diameter: 5,segments: 32,sideOrientation: Mesh.DOUBLESIDE},this.scene)// textureconst videoTexture = new VideoTexture('video',this.video,this.scene,false,true,VideoTexture.TRILINEAR_SAMPLINGMODE,{autoPlay: true,autoUpdateTexture: true})// materialconst videoMat = new StandardMaterial('videoMat', this.scene)videoMat.diffuseTexture = videoTexturevideoMat.emissiveTexture = videoTexturethis.videoBall.material = videoMat}/** setter&getter**/// videoElementpublic get video (): HTMLVideoElement {return this._video}public set video (value: HTMLVideoElement) {this._video = value}
}

我编译了一份es5版本的js文件,有其他版本需要的话去ts官网playground中自行编译

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }return new (P || (P = Promise))(function (resolve, reject) {function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }step((generator = generator.apply(thisArg, _arguments || [])).next());});
};
var __generator = (this && this.__generator) || function (thisArg, body) {var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;function verb(n) { return function (v) { return step([n, v]); }; }function step(op) {if (f) throw new TypeError("Generator is already executing.");while (_) try {if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;if (y = 0, t) op = [op[0] & 2, t.value];switch (op[0]) {case 0: case 1: t = op; break;case 4: _.label++; return { value: op[1], done: false };case 5: _.label++; y = op[1]; op = [0]; continue;case 7: op = _.ops.pop(); _.trys.pop(); continue;default:if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }if (t[2]) _.ops.pop();_.trys.pop(); continue;}op = body.call(thisArg, _);} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };}
};
import videojs from 'video.js';
import 'videojs-contrib-hls';
import { Color4, Engine, Scene, DeviceOrientationCamera, Vector3, Mesh, CreateSphere, VideoTexture, StandardMaterial } from '@babylonjs/core';
import '@babylonjs/inspector';
/***vr直播组件,* @param canvas*        babylonjs运行所需要的canvas** @param videoSrc*        视频直播接流地址** @return vr直播组件的实例*/
var VrLive = /** @class */ (function () {function VrLive(canvas, videoSrc) {if (canvas && videoSrc) {this.canvas = canvas;this.video = document.createElement('video');this.videoSrc = videoSrc;this.initVideo();}}// initBabylonVrLive.prototype.initBabylon = function () {this.initEngine(this.canvas);this.initVideoBall();this.initCamera();this.debuggerLayer();};// 初始化videoVrLive.prototype.initVideo = function () {this.creatVideoElement(); // 创建视频标签if (!this.isIPhone()) {var vrVideo_1 = videojs(this.video, // id或者videoElement{}, // 配置项function () {vrVideo_1.play();} // 加载完成后的回调);}};// 创建视频标签VrLive.prototype.creatVideoElement = function () {// video标签this.video.id = 'vrVideo';this.video.muted = true;this.video.autoplay = true;this.video.src = this.videoSrc;// videoSorce标签var videoSorce = document.createElement('source');videoSorce.type = 'application/x-mpegURL';videoSorce.src = this.videoSrc;this.video.appendChild(videoSorce);this.video.playsInline = true;this.video.style.width = '100vw';this.video.style.height = '10vh';this.video.style.position = 'absolute';this.video.style.top = '0';this.video.style.left = '0';this.video.style.zIndex = '20';this.video.controls = true;document.getElementsByTagName('body')[0].appendChild(this.video); // 添加进domthis.video.style.display = 'none';};// isIPhoneVrLive.prototype.isIPhone = function () {var u = navigator.userAgent;return !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) || u.indexOf('iPhone') > -1 || u.indexOf('iPad') > -1; // ios终端};// 初始化引擎VrLive.prototype.initEngine = function (canvas) {return __awaiter(this, void 0, void 0, function () {var assumedFramesPerSecond, earthGravity;return __generator(this, function (_a) {this.canvas = canvas; // 初始化canvasthis.engine = new Engine(this.canvas, true, {}, true);this.scene = new Scene(this.engine); // 初始化场景//   this.scene.clearColor = new Color4(0.784, 0.878, 1, 1)this.scene.clearColor = new Color4(1, 1, 1, 1);assumedFramesPerSecond = 60;earthGravity = -9.34;this.scene.gravity = new Vector3(0, earthGravity / assumedFramesPerSecond, 0); // 设置场景重力(模拟地球)this.scene.collisionsEnabled = true; // 开启碰撞检测this.RenderLoop(); // 执行渲染循环return [2 /*return*/];});});};// 渲染循环VrLive.prototype.RenderLoop = function () {var _this = this;// 添加窗口变化事件监听window.addEventListener('resize', function () {_this.engine.resize();});// 执行循环this.engine.runRenderLoop(function () {_this.scene.render();});};// debuggerLayer(Shift+Ctrl+Alt+I)VrLive.prototype.debuggerLayer = function () {var _this = this;window.addEventListener('keydown', function (ev) {if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) {if (_this.scene.debugLayer.isVisible()) {_this.scene.debugLayer.hide();}else {_this.scene.debugLayer.show({ embedMode: true });}}});};// 初始化相机VrLive.prototype.initCamera = function () {this.camera = new DeviceOrientationCamera('vrLiveCamera', new Vector3(0, 0, 0), this.scene);// eslint-disable-next-line dot-notationthis.camera.inputs.remove(this.camera.inputs.attached['keyboard']);this.camera.attachControl(this.canvas, true);this.camera.fov = 1.5;this.camera.fovMode = DeviceOrientationCamera.FOVMODE_HORIZONTAL_FIXED;};// 初始化视频球VrLive.prototype.initVideoBall = function () {// meshthis.videoBall = CreateSphere('VideoBall', {diameter: 5,segments: 32,sideOrientation: Mesh.DOUBLESIDE}, this.scene);// texturevar videoTexture = new VideoTexture('video', this.video, this.scene, false, true, VideoTexture.TRILINEAR_SAMPLINGMODE, {autoPlay: true,autoUpdateTexture: true});// materialvar videoMat = new StandardMaterial('videoMat', this.scene);videoMat.diffuseTexture = videoTexture;videoMat.emissiveTexture = videoTexture;this.videoBall.material = videoMat;};Object.defineProperty(VrLive.prototype, "video", {/** setter&getter**/// videoElementget: function () {return this._video;},set: function (value) {this._video = value;},enumerable: false,configurable: true});return VrLive;
}());
export default VrLive;

4.开发调试

调试过程中如果服务器和前端项目不在同一个域名下,拉流会产生跨域问题,所以开发过程中我在浏览器上安装了Allow CORS: Access-Control-Allow-Origin插件,可以用来解除浏览器跨域限制,手机预览可用fiddler处理跨域后开代理,手机wifi连接代理进行访问。具体参考

Fiddler解决跨域问题https://blog.csdn.net/weixin_43409011/article/details/114121360

Fiddler APP抓包手机代理设置https://blog.csdn.net/weixin_43145997/article/details/123594342


5.发布

将项目build好的dist文件夹内容放入之前搭建的nginx服务器下html目录中即可访问,也可以另外搭建服务器后通过nginx配置转发,需要保证客户端访问页面与拉流地址在同一域名端口下即可。

【瞎鼓捣】web前端全景直播相关推荐

  1. web前端 html5 直播功能开发

    一. video.js 视频播放 github:https://github.com/videojs/video.js 基础代码: <script src="https://unpkg ...

  2. HTML5纯Web前端也能开发直播,不用开发服务器(使用face2face)

    前段时间转载了某位大神的一篇文章,开发Web版一对一远程直播教室只需30分钟 - 使用face2face网络教室.非常有意思.看起来非常简单,但作为一名前端开发人员来说,还是有难度.因为要开发服务器端 ...

  3. 阿里云视频直播 web前端[移动端] Aliplayer的简单案例

    阿里云视频直播 web前端[移动端] Aliplayer的简单案例 最近做了一个功能就是播放后台提供的各种直播视频,格式在未确定的情况下,刚开始以为简单的一个video标签就能播放视频,后面才发现各种 ...

  4. php直播前端,全民直播高薪诚聘 PHP 开发工程师、web 前端开发工程师啦~~

    各位大牛们,全民直播诚邀您的加入~~~ 1 . Web 前端开发工程师 15K+ 上不封顶哦~~~ 岗位职责 准确理解产品需求.交互文档或原型, 进行 web 产品前端开发: 优化用户体验,修正项目中 ...

  5. BAT前端老鸟总结:未来几年web前端发展四大趋势前瞻

    经过近5年的快速发展,目前前端开发技术栈已经进入成熟期.在React和Vue等框架出现后,前端在代码开发方面的复杂度已经基本得到解决,再加上Node解决前后端分离,前端技术栈本身其实已经非常成熟.因此 ...

  6. [转] WEB前端学习资源清单

    常用学习资源 JS参考与基础学习系列 [MDN]JS标准参考 es6教程 JS标准参考教程 编程类中文书籍索引 深入理解JS系列 前端开发仓库 <JavaScript 闯关记> JavaS ...

  7. web前端零基础入门学习!前端真不难!

    现在互联网发展迅速,前端也成了很重要的岗位之一,许多人都往前端靠拢,可又无能为力,不知所措,首先我们说为什么在编程里,大家都倾向于往前端靠呢?原因很简单,那就是,在程序员的世界里,前端开发是最最简单的 ...

  8. 360视频云Web前端HEVC播放器实践剖析

    360视频云前端团队围绕HEVC前端播放及解密实现了一套基于WebAssembly.WebWorker的通用模块化Web播放器,在LiveVideoStackCon2019深圳的演讲中360奇舞团We ...

  9. RequestBody获取前端数据_360视频云Web前端HEVC播放器实践剖析

    360视频云前端团队围绕HEVC前端播放及解密实现了一套基于WebAssembly.WebWorker的通用模块化Web播放器,在LiveVideoStackCon2019深圳的演讲中360奇舞团We ...

最新文章

  1. 用c语言编写通讯录程序,学C三个月了,学了文件,用C语言写了个通讯录程序
  2. 单文档程序创建的时候,标题栏的无标题怎么可以去掉,并且改成自己想要的字符...
  3. ssh 远程登陆异常SSH_EXCHANGE_IDENTIFICATION及解决过程
  4. Struts2的下载安装
  5. mysql建表时外检怎么创建_MySQL创建表时加入的约束以及外键约束的的意义
  6. LintCode 387: Smallest Difference
  7. SendMessage、PostMessage原理和源代码详解
  8. wifi6无线网卡驱动linux,linux2.6.8内核装intel wifi link 5100无线网卡驱动的问题?
  9. ShuffleNet在Caffe框架下的实现
  10. Laravel8 小程序手机号获取验证码登录
  11. 什么是ribbon?
  12. oracle rman迁移spfile,RMAN 异机迁移实战操作-附加常用命令
  13. 【转】用winpcap实现局域网DNS欺骗之一(基础知识)
  14. Linux下pppd拨号脚本配置
  15. 计算机课里的余数是什么,余数
  16. Excel表格中的三维气泡图,你会做吗?
  17. 计算机组成原理实验所用的指令,计算机组成原理实验报告-控制器及微指令系统的操作与运用...
  18. Java反射的底层原理,以及Java反射的性能分析及优化
  19. 纯css锚点跳转过渡效果 - 神奇的scroll-behavior属性
  20. 刺激越多效果越好?无创神经调控技术(rTMS)缓解疼痛

热门文章

  1. c++ 类的头文件和源文件拆分
  2. 某程序员转行前的感慨 -【告别程序员生涯 】
  3. 人体的99个秘密,男女生都该看,绝对受益终生 !!!
  4. 大不列颠泰迪熊加入PUBG 手游
  5. 【电赛】2019电子设计竞赛 纸张计数显示装置(F题)
  6. 炫彩界面库-模仿360安全卫士8.8,支持透明,换肤
  7. 小程序使用vant/weapp
  8. Kriging 克里金算法Java实现
  9. 【职场沟通】职场如何赞美别人?
  10. pve 加大local容量_许迎果 第189期 PVE虚拟平台的存储策略和分区调整