封装像素流送功能为Vue组件

5,747 阅读8分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

介绍

在本文中,我们将详细描述如何将UE(Unreal Engine)像素流送功能集成到前端工程中。首先,我将解释UE像素流送的工作原理,讲述如何提取UE像素流送功能的核心代码,并将其封装为一个可重用的插件或模块。我们将探讨UE引擎中的扩展机制和自定义功能的实现方式,以便将UE像素流送功能集成到前端工程中。

本文提到的Vue组件工程Pixel-streaming-layer我放到github上,以便后续大家下载交流。

pixel-streaming-layer1.gif

使用工具

工具版本号
UE5.1安装pixel-streaming后打包为应用
Pixel Streaming Interface5.1UE像素流送演示工程包
vue3.X前端框架
vue-cli5.0.XVue工程脚手架

实现思路

介绍

在进入开发之前,为了对组件的功能有更加充分的认知,有必要了解像素流送的整个执行过程,这里只做入门版介绍,专业版介绍请看这位老哥写的

整个过程可以分为5个阶段,客户浏览器端和UE应用端通过信令服务器进行协商,然后形成稳定的P2P连接,在WebRtc协议的保证下客户终端获取媒体流,捕获用户行为发送指令给UE应用端,UE根据指令调整画面形成反馈。后面我对每个阶段进行了更加详细的描述。

Untitled.png

阶段描述

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连接,同时信令服务器更新会话状态。

核心业务逻辑

从上文的描述我们可以知道,组件所要实现的,是客户终端持续获取视频音频媒体流和发送指令的这个阶段的功能。

建立流媒体连接具体的业务逻辑如下:

Untitled 1.png

实现步骤

1. 下载工程包

从UE官方获取Pixel Streaming Infrastructure,这个工程里包含了信令服务器和浏览器前端连接实例,本文使用的是UE5.1版本。有需要可以直接在github下载 各版本地址入口

2.创建工程

创建Vue3工程pixel-streaming-layer, 目录如下。关于vue组件库的小白开发教程可以看这里

Untitled 2.png

3. 获取核心代码

在步骤1的工程中按下图目录找到两个文件app.js和webRtcPlayer.js,这是最终vue组件的核心文件。在vue3工程创建目录“src/components/pixel-stream-layer”, 并把两个文件放到这里面。

(1)app.js 核心业务代码,更新为core.js

(2)webRtcPlayer 通用的WebRTC播放器

Untitled 3.png

4. 代码封装

  1. 在目录“src/components/pixel-stream-layer” 新建index.vue作为组件的入口,引入core.js。core.js对初始化、调整画面、播放、暂停等方法做了封装,因此可以在index.vue直接引用。

    import * 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()
      },
      //...
    }
    
  2. core.js内部则直接引入了webRtcPlayer.js

    import 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. 打包组件

  1. 打包入口文件为 src/components/index.js

  2. 由于我这边是以组件库的形式创建的工程,所以在入口文件中,会以组件库中一个组件的方式引入

    import 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
    }
    
  3. package.json打包指令如下

    -target lib:指定构建的目标为库,即将组件构建为可被其他项目引用的独立库。

    -dest lib:指定构建输出的目录为 lib,即构建后的文件将被输出到 lib 目录下。

    "build:component": "vue-cli-service build --target lib --dest lib src/components/index.js",
    

6. 在实际项目中使用组件

  1. 全局引入

    // 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"/>
    
  2. 创建实例

     <pixel-stream-layer ref="pslayer" server-url="http://192.168.1.254"/>
    

7. 测试功能

pixel-streaming-layer2.gif

核心代码改造

因业务需要,我对核心业务代码core.js进行了改造。

  1. 调整init,将流媒体地址和可配置项变成构造参数

    /**
     * 初始化功能
     * @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
    }
    
  2. 暴露公共方法

    /**
     * 调整画面分辨率以适应当前容器尺寸
     * @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)
    }
    
  3. 组件入口代码,基于前两步骤的改造,组件就可以提供对应的配置参数和公共方法

    import * 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()
        }
      }
    }
    

待改进内容

  1. 更多的配置项

    事实上在本文使用的UE5.1版本,提供以下的初始配置项,都可以调整后作为构造参数,看具体项目需要

    Label描述参数名说明
    Use microphoneuseMic是否使用麦克风,语音录入功能只能在localhost和https协议下才能进行
    prefer SFUpreferSFU媒体流是否使用SFU作为媒体流传输方式
    Force TURNForceTURN强制使用 TURN服务器作为中继来传输媒体流
    Force mono audioForceMonoAudio强制单声道
    Control SchemecontrolScheme控制模式有2种。 "Hoving Mouse":玩家可以在操作游戏时使用鼠标进行其他操作,如切换窗口、调整音量等。
    "Locked Mouse":玩家在操作游戏时鼠标不能离开游戏窗口
    Hide Browser CursorhideBrowserCursor隐藏浏览器光标
    Request KeyFrame要求视频编码器生成关键帧
    offerToReceive主动发起offer
    noWatermark是否去除UE水印
  2. 控制权限

    由于UE应用实例在运行中是相当耗资源的,在目前的情况下不可能给所有访问的用户开单独的实例,所以会有多用户操作同一个实例的情况。因此需要增加配置项,去除一部分用户的控制权限,即只能看而不能控制。这块实现不难,只要把发送指令相关的逻辑加屏蔽条件即可。

  3. 增加调试模式

    目前是调试面板,后续可以考虑通过is-debug配置属性的方式增加调试面板,将画面和操作调整到最佳状态。

相关链接

pixel-streaming-layer源代码

UE5.1 + Vue3像素流,保姆级教程

一文看懂WebRtc建连过程

GPT3.5+MetaHuman 实现工程

从零开始:Vue cli3 库模式搭建组件库并发布到npm