本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
介绍
在本文中,我们将详细描述如何将UE(Unreal Engine)像素流送功能集成到前端工程中。首先,我将解释UE像素流送的工作原理,讲述如何提取UE像素流送功能的核心代码,并将其封装为一个可重用的插件或模块。我们将探讨UE引擎中的扩展机制和自定义功能的实现方式,以便将UE像素流送功能集成到前端工程中。
本文提到的Vue组件工程Pixel-streaming-layer我放到github上,以便后续大家下载交流。
使用工具
工具 | 版本号 | |
---|---|---|
UE | 5.1 | 安装pixel-streaming后打包为应用 |
Pixel Streaming Interface | 5.1 | UE像素流送演示工程包 |
vue | 3.X | 前端框架 |
vue-cli | 5.0.X | Vue工程脚手架 |
实现思路
介绍
在进入开发之前,为了对组件的功能有更加充分的认知,有必要了解像素流送的整个执行过程,这里只做入门版介绍,专业版介绍请看这位老哥写的。
整个过程可以分为5个阶段,客户浏览器端和UE应用端通过信令服务器进行协商,然后形成稳定的P2P连接,在WebRtc协议的保证下客户终端获取媒体流,捕获用户行为发送指令给UE应用端,UE根据指令调整画面形成反馈。后面我对每个阶段进行了更加详细的描述。
阶段描述
1. 准备阶段
- UE项目配置:首先,需要在Unreal Engine项目中启用和配置像素流式传输插件,设置适当的视频质量和性能参数。
- 信令服务器设置:为了建立UE服务器和客户端之间的连接,需要一个信令服务器。信令服务器负责交换网络配置信息(如IP地址和端口),以及初始化WebRTC会话。
- 部署UE应用:将UE应用部署到支持像素流式传输的服务器上。服务器需要有足够的图形处理能力来渲染3D场景。
2. 建立连接
- 客户端请求连接:客户端(通常是Web浏览器中的一个页面)通过信令服务器向UE服务器发送连接请求。
- 协商WebRTC连接:使用信令服务器交换所需的WebRTC参数,包括SDP(会话描述协议)消息和ICE(交互式连接建立)候选,以建立一个稳定的P2P(点对点)视频流连接。
3. 流式传输
- UE渲染和编码:UE服务器渲染3D场景,并将渲染的帧实时编码为视频流。
- 通过WebRTC发送视频流:编码后的视频流通过建立的WebRTC连接发送给客户端。WebRTC技术确保了流的实时性和高效性,支持跨网络的低延迟传输。
- 客户端解码和显示:客户端接收视频流,进行解码,并在用户界面中显示渲染的场景。
4. 交互
- 客户端输入处理:客户端可以捕获用户输入(如键盘、鼠标或触摸事件)并通过WebRTC连接发送回UE服务器。
- UE服务器响应:UE服务器根据收到的输入更新场景状态,下一帧渲染将反映这些更改,实现交互式体验。
5. 结束连接
- 断开连接:当会话结束或用户离开时,客户端和服务器均可关闭WebRTC连接,同时信令服务器更新会话状态。
核心业务逻辑
从上文的描述我们可以知道,组件所要实现的,是客户终端持续获取视频音频媒体流和发送指令的这个阶段的功能。
建立流媒体连接具体的业务逻辑如下:
实现步骤
1. 下载工程包
从UE官方获取Pixel Streaming Infrastructure ,这个工程里包含了信令服务器和浏览器前端连接实例,本文使用的是UE5.1版本。有需要可以直接在github下载 各版本地址入口
2.创建工程
创建Vue3工程pixel-streaming-layer, 目录如下。关于vue组件库的小白开发教程可以看这里。
3. 获取核心代码
在步骤1的工程中按下图目录找到两个文件app.js和webRtcPlayer.js,这是最终vue组件的核心文件。在vue3工程创建目录"src/components/pixel-stream-layer", 并把两个文件放到这里面。
(1)app.js 核心业务代码,更新为core.js
(2)webRtcPlayer 通用的WebRTC播放器
4. 代码封装
-
在目录"src/components/pixel-stream-layer" 新建index.vue作为组件的入口,引入core.js。core.js对初始化、调整画面、播放、暂停等方法做了封装,因此可以在index.vue直接引用。
jsximport * as Core from './core.js' //... mo1unted () { this.videoInstance = Core.init() }, methods:{ /** * 向UE场景派发指令 * @param {String} message 指令内容,比如'openDoor ID1' */ emitMessageToUE (message) { Core.emitUIInteraction(message) }, /** * 播放视频 * @public */ play () { Core.play() }, //... }
-
core.js内部则直接引入了webRtcPlayer.js
jsximport webRtcPlayer from './webRtcPlayer' export function init (config) { // ... return webRtcPlayerObj } /** * 初始化WebRTC播放器实例 * @param {HTML} htmlElement 容器标签 * @param {*} config 配置参数 * @returns {DOM} */ function setupWebRtcPlayer (htmlElement, config) { webRtcPlayerObj = new webRtcPlayer({ ...config, startVideoMuted: true }) //... }
5. 打包组件
-
打包入口文件为 src/components/index.js
-
由于我这边是以组件库的形式创建的工程,所以在入口文件中,会以组件库中一个组件的方式引入
jsximport PixelStreamLayer from './pixel-stream-layer/index.vue' const components = { PixelStreamLayer } function install (Vue) { const keys = Object.keys(components) keys.forEach((name) => { const component = components[name] Vue.component(component.name || name, component) }) } export default { install, ...components }
-
package.json打包指令如下
-target lib
:指定构建的目标为库,即将组件构建为可被其他项目引用的独立库。-dest lib
:指定构建输出的目录为lib
,即构建后的文件将被输出到lib
目录下。jsx"build:component": "vue-cli-service build --target lib --dest lib src/components/index.js",
6. 在实际项目中使用组件
-
全局引入
jsx// main.js全局注册 import { createApp } from 'vue' const app = createApp(App) // npm部署到私有库了 import PixelStreamLayer from '@zkzc/pixel-streaming-layer' app.use(PixelStreamLayer) // 引入样式 import '@zkzc/pixel-streaming-layer/lib/pixel-streaming-layer.css' <pixel-stream-layer ref="pslayer" :server-url="serverURL"/>
-
创建实例
html<pixel-stream-layer ref="pslayer" server-url="http://192.168.1.254"/>
7. 测试功能
核心代码改造
因业务需要,我对核心业务代码core.js进行了改造。
-
调整init,将流媒体地址和可配置项变成构造参数
jsx/** * 初始化功能 * @param {Object} [config={}] * @param {String} serverUrl 视频流服务地址 * @param {Boolean} [autoOfferToReceive=true] 是否前端主动发起offer * @return webRtcPlayer */ export function init (config) { // 流服务连接地址 connectURL = config.serverUrl // 是否前端主动发起offer autoOfferToReceive = setDefaultTrue(config.autoOfferToReceive) // 监听各种stream消息并处理 registerMessageHandlers() // 声明各种与Stream交流的Message类型 populateDefaultProtocol() // 初始化冻结层,当视频画面停止更新时会出现 setupFreezeFrameOverlay() // 将每个按键操作写入到操作序列,等待逐个执行 registerKeyboardEvents() // 开始核心逻辑 start(false) return webRtcPlayerObj }
-
暴露公共方法
jsx/** * 调整画面分辨率以适应当前容器尺寸 * @public */ export function updateViewToContainer () { const playerElement = document.getElementById('player') const descriptor = { 'Resolution.Width': playerElement.clientWidth, 'Resolution.Height': playerElement.clientHeight } emitCommand(descriptor) } /** * 开始播放 */ export function play () { connect() startAfkWarningTimer() } /** * 停止播放 */ export function stop () { if (webRtcPlayerObj) { webRtcPlayerObj.close() } } /** * 发起一个指令,针对UE关卡蓝图 * @param {String} descriptor */ function emitCommand (descriptor) { emitDescriptor('Command', descriptor) } /** * 发起一个交互操作,针对UE暴露的方法 * @param {String} descriptor */ export function emitUIInteraction (descriptor) { emitDescriptor('UIInteraction', descriptor) }
-
组件入口代码,基于前两步骤的改造,组件就可以提供对应的配置参数和公共方法
jsximport * as Core from './core.js' export default { name: 'PixelStreamLayer', props: { serverUrl: { // 流媒体地址 type: String, required: true }, config: { // 各种配置参数 type: Object } }, data () { return { videoInstance: null } }, mounted () { this.videoInstance = Core.init({ serverUrl: this.serverUrl //...其他配置项 }) }, methods: { /** * 向UE场景派发指令 * @param {String} message 指令内容,比如'openDoor ID1' */ emitMessageToUE (message) { Core.emitUIInteraction(message) }, // 将画面填满窗口 fillView () { Core.updateViewToContainer() }, // 页面在加载后可自动播放 play () { Core.play() }, // 组件销毁前可自动停止 stop () { Core.stop() } } }
待改进内容
-
更多的配置项
事实上在本文使用的UE5.1版本,提供以下的初始配置项,都可以调整后作为构造参数,看具体项目需要
Label描述 参数名 说明 Use microphone useMic 是否使用麦克风,语音录入功能只能在localhost和https协议下才能进行 prefer SFU preferSFU 媒体流是否使用SFU作为媒体流传输方式 Force TURN ForceTURN 强制使用 TURN服务器作为中继来传输媒体流 Force mono audio ForceMonoAudio 强制单声道 Control Scheme controlScheme 控制模式有2种。 "Hoving Mouse":玩家可以在操作游戏时使用鼠标进行其他操作,如切换窗口、调整音量等。 "Locked Mouse":玩家在操作游戏时鼠标不能离开游戏窗口 Hide Browser Cursor hideBrowserCursor 隐藏浏览器光标 Request KeyFrame 要求视频编码器生成关键帧 offerToReceive 主动发起offer noWatermark 是否去除UE水印 -
控制权限
由于UE应用实例在运行中是相当耗资源的,在目前的情况下不可能给所有访问的用户开单独的实例,所以会有多用户操作同一个实例的情况。因此需要增加配置项,去除一部分用户的控制权限,即只能看而不能控制。这块实现不难,只要把发送指令相关的逻辑加屏蔽条件即可。
-
增加调试模式
目前是调试面板,后续可以考虑通过is-debug配置属性的方式增加调试面板,将画面和操作调整到最佳状态。