简介:教程围绕Unreal Engine 5与Vue 3的像素流集成,面向希望将高保真3D场景实时投递到Web端的游戏开发者、前端工程师及数字可视化从业者。内容涵盖Vue 3的Composition API、Suspense异步组件,以及UE5的Lumen全局光照与Nanite虚拟几何体,深入讲解如何借助像素流技术把UE5渲染画面同步至浏览器,并配以Vite工程配置、目录结构、依赖管理等完整落地流程。资源包共538个文件,以JS/TS脚本、Vue组件、JSON配置、Markdown笔记及工程配置文件为主,整体约8.87MB,代码与文档分层清晰,便于对照学习。已有1242人参与学习,教程既覆盖基础配置,也包含多领域应用场景,适合用于游戏开发、虚拟现实、建筑可视化与产品演示等项目的技术预研与实战参考。

1. 像素流最难的不是渲染,是把 WebRTC 信令接对

像素流这三个字,会让不少人下意识当成“推流 + 拉流”来理解,实际链路跟视频直播完全是两回事。UE5 的 Pixel Streaming 插件只负责把渲染画面实时编码成 H.264,浏览器端通过 WebRTC 的 RTCPeerConnection 直接接收;信令服务器只在握手阶段居中传递 SDP 和 ICE 候选,握手结束之后不碰媒体数据。所以一个像素流项目能不能跑通,七成取决于信令交换和 ICE 选路,而不是 UE 场景做得多么精致。下面这套流程以包含 Vue3 前端、node 信令服务器和 UE5 启动配置的完整项目为底,从启动参数、信令搭建、组件封装到双向消息与链路诊断,按实际动手顺序过一遍。适合用过 UE 蓝图、有 Vue 基础,准备做数字孪生、建筑可视化或线上产品演示的开发。

2. Ue5 像素流插件启动参数与信令服务器搭建

2.1 像素流链路里的三个角色:UE、信令、浏览器

在一个像素流会话里,媒体的流向是:UE 端编码 → 浏览器解码显示;信令的流向是:浏览器 WebSocket 连信令服务器 → 信令服务器把浏览器发来的 offer 转给 UE → UE 返回 answer。整个过程有一个容易被忽略的细节:WebRTC 的媒体流走的是 P2P,信令服务器只在建立连接时出现几秒钟。这个特性决定了排错顺序——页面视频黑屏,先看 ICE 候选是否成对,而不是先怀疑 Vue 组件写错。

三个角色的边界要分清楚。UE 端需要具备 H.264 硬件编码能力;信令服务器通常跑在一台局域网或公网可达的 node 进程上;前端只需要一个支持 WebRTC 的浏览器,Chrome、Edge、Firefox 都行。这三个角色里离代码最近的是 Vue3 前端,但最先要启动的是 UE 和信令服务器,因为前端在连接成功之前,控制台只会反复报 WebSocket 连接失败。

2.2 插件启用与关键启动参数

打开 UE5 编辑器,在 Edit → Plugins 里搜索 Pixel Streaming,启用后按提示重启。该插件在 UE5 内置,不需要额外下载,但默认不开启。启用后不是直接打包就完事,还要决定跑在哪台机器、以什么分辨率渲染、是否离屏。这几个参数直接决定画面质量和编码压力。

参数 作用 使用场景
-PixelStreamingPort=8888 指定 WebRTC 媒体端口,默认 8888 多实例部署时为每台 UE 分配不同端口
-RenderOffScreen 离屏渲染,不弹窗口 Linux 服务器、无头环境必加
-ResX=1920 -ResY=1080 设置渲染分辨率 高分辨率输出或限制带宽时调整
-Windowed -ForceRes 强制窗口模式并应用分辨率 Windows 调试时预览 UE 画面
-PixelStreamingIP= 指定信令服务器地址 跨网段时显式填写,通常可省略

打包后的可执行文件可以直接带参数启动:

# Windows 调试环境:前台窗口模式
UEProject.exe ProjectName -Game -PixelStreamingPort=8888 -ResX=1920 -ResY=1080 -Windowed -ForceRes

# Linux 无头渲染:必须离屏,画面走 Pixel Streaming 到浏览器
./UEProject ProjectName -RenderOffScreen -PixelStreamingPort=8888 -ResX=1920 -ResY=1080

逻辑说明: -Game 表示直接进入游戏模式而不是编辑器界面,像素流线上场景一般不进编辑器,这更接近真实部署。 -RenderOffScreen 在 Linux 上几乎是必须的,因为服务器没有显示器;Windows 调试时可以先不加,用原生窗口确认场景加载成功再切离屏。 -ResX/-ResY 同时影响编码输入分辨率和带宽占用,如果主机只有集显,1080p30 都会很吃力,要优先保证独显的 NVENC 编码器可用。

参数写熟练之后,建议把启动命令存成批处理脚本。这套项目里出现的 parser.cmd 、 rollup.cmd 就是这类启动脚本的常见命名:一个解析配置后拉起 UE,一个负责前端资源打包。Windows 环境我习惯再配一个类似 openChrome.applescript 的小脚本,让浏览器自动打开对应页面,省去手动输入地址这一步。

2.3 信令服务器的两种跑法

信令服务器有两个来源:一是像素流插件自带的 SignalServer,二是教程包里常见的 node 版本。这份项目里的 index.cjs 就是 node 版信令服务器的入口文件, nanoid.cjs 用于在浏览器与 UE 握手时生成会话 ID,避免并发连接时 ID 冲突。跑法非常直接:

npm install
node index.cjs

默认监听 80 端口。如果本机 80 已被占用,带端口参数启动:

node index.cjs --httpPort 8080

参数说明: npm install 根据 package.json 安装信令服务器依赖; node index.cjs 启动后同时监听 HTTP 和 WebSocket 两个入口,前者用于浏览器访问管理页,后者用于交换 SDP 与 ICE。 --httpPort 是常见 node 信令实现的参数名,不同版本写法略有差异,如果你的包不认这个参数,直接看 index.cjs 里 process.argv 的解析段,改默认值即可。

启动完成后,浏览器访问 http://127.0.0.1:80/ 能打开信令服务器管理页,页面会显示当前已连接的 UE 实例列表和连接地址。从这里能看到 STUN 地址等网络信息,是后面排错的第一站。

2.4 联通性验证

UE 启动后,信令服务器管理页的 UE 实例列表会多出一条记录。先用 curl 确认信令服务器进程本身正常:

curl -s http://127.0.0.1:80/ | head -n 20

能返回 HTML 内容而不是 connection refused ,说明 HTTP 服务在跑。再确认 WebSocket 端口也通,可以用 websocat 探测:

websocat ws://127.0.0.1:80/ --ping-interval=5

--ping-interval=5 表示每 5 秒发一次 WebSocket ping,用来观察连接是否被服务端主动断开。这两个命令把“进程活着”和“WebSocket 可达”分开验证:前者只代表 TCP 端口有进程监听,后者才是前端 SDK 真正依赖的消息通道。如果 WebSocket 连不上,Vue 页面会出现长期 connecting 状态,这是排查时最常见的起点。另外防火墙要把 8888/udp 放行,信令通了但媒体端口不通,前端依然黑屏,而且报错信息往往不具备可读性。

3. Vue3 工程接入像素流:Vite 项目与 StreamPlayer 组件

3.1 搭建 Vite 工程并确认目录结构

Vue3 工程骨架由 Vite 生成,这也是官方推荐的创建方式。先拉工程再安装像素流前端依赖:

npm create vite@latest pixel-client -- --template vue
cd pixel-client
npm install
npm install @epicgames/pixel-streaming-frontend

第三行安装的是像素流官方前端 SDK。如果你用的教程包自带 node_modules,依赖里已经有这个包,不需要重复安装。装完后工程里会看到 vite.config.js 、 package.json 、 package-lock.json 、 yarn.lock 、 index.browser.cjs 这类文件, index.browser.cjs 通常是打包工具给浏览器侧用的入口构建,看到它基本可以确认前端 SDK 已经完整落地。

需要注意 vite.config.js 的默认监听地址是 127.0.0.1 ,只能当前机器访问。像素流场景里浏览器和 UE 经常不在同一台机器,所以 dev server 要绑定到 0.0.0.0 :

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    host: '0.0.0.0',
    port: 5173
  }
})

host: '0.0.0.0' 让 Vite 监听所有网卡,局域网其他机器可以用 http://本机IP:5173 访问; port 按需要改。 public/index.html 里保留 <div id="app"></div> 挂载点,Vue 实例挂载逻辑在 src/main.js ,这些属于 Vue3 标准结构,不需要动改动。如果页面将来部署到 Nginx,这个配置只影响开发期, npm run build 之后用 dist 目录即可。

3.2 把像素流封装成 StreamPlayer 组件

像素流前端的核心对象是官方 SDK 里的 PixelStreaming 类,它负责 WebSocket 信令、RTCPeerConnection 生命周期、ICE 候选收集以及输入事件转发。Vue3 里只需要把它封装进一个组件,并处理好挂载与销毁:

<!-- src/components/StreamPlayer.vue -->
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { PixelStreaming } from '@epicgames/pixel-streaming-frontend'

const videoRef = ref(null)
let streaming = null

const props = defineProps({
  signallingUrl: { type: String, default: 'ws://127.0.0.1:80/' },
  rtcConfig: { type: Object, default: () => ({}) }
})

function connect() {
  streaming = new PixelStreaming({
    videoElement: videoRef.value,
    signallingUrl: props.signallingUrl,
    rtcConfig: props.rtcConfig
  })
}

onMounted(() => connect())
onBeforeUnmount(() => {
  streaming?.dispose?.()
  streaming = null
})
</script>

<template>
  <video ref="videoRef" autoplay playsinline muted></video>
</template>

代码说明: videoRef 指向 <video> 元素, PixelStreaming 初始化后会自动把它接到远端流上; signallingUrl 是信令服务器 WebSocket 地址,默认指向本机 80 端口; rtcConfig 用来注入 STUN/TURN,后面单独讲。 autoplay playsinline muted 三个属性是移动端自动播放的前提,缺一个都会出现首帧停滞的情况。销毁时机用 onBeforeUnmount 处理,路由切换或组件卸载时释放 PeerConnection,避免页面堆叠多个连接。

如果不依赖官方 SDK,手写 RTCPeerConnection 的时序也要清楚:先 new RTCPeerConnection(rtcConfig) ,再 pc.addTransceiver('video', { direction: 'recvonly' }) ,创建 createOffer() 后经 WebSocket 发给信令服务器,UE 返回 answer 后 setRemoteDescription() ,ICE 候选则通过 onicecandidate 持续交换。实际项目我建议直接封装官方 SDK,协议细节容易出错,手写适合学习信令流程或深度定制协议时用。

3.3 ICE 服务器与局域网直连的取舍

像素流默认场景是浏览器与 UE 在同一局域网,ICE 会自动选择 host 类型候选,延迟最低。一旦浏览器在外网,就要考虑 STUN 和 TURN。STUN 让两端各自发现公网地址,NAT 类型允许时媒体流仍可直连;只有两侧 NAT 都打洞失败,才会退化到 TURN 中继。所以生产环境建议自建 coturn,不要把公共 STUN 直接塞进核心链路。

// rtcConfig 常见写法
const rtcConfig = {
  iceServers: [
    { urls: 'stun:stun.example.com:3478' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'demo',
      credential: 'demo-pass'
    }
  ],
  iceCandidatePoolSize: 8
}

参数说明: iceServers 数组按优先级写入,STUN 只做探测,TURN 才承担媒体转发; iceCandidatePoolSize 预生成候选池减小首帧时间,但值太大对网络资源不友好,8 到 12 比较常见。 username 和 credential 对应 coturn 里 turnadmin 创建的用户。TURN 中继会增加 10-20ms 级延迟,属于兜底方案;如果页面报 ICE connection failed ,先确认 STUN 通不通,再看 TURN 配置,不要一上来就把所有流量劫持到中继。

3.4 常用参数与浏览器布局问题

接入完成后,把像素流前端常用参数整理成一张表,方便调带宽和画质。

参数 示例 作用
signallingUrl ws://192.168.1.10:80/ 信令地址,前端连不上 UE 的 90% 原因在这里
rtcConfig.iceServers 见上方代码 控制 P2P 直连或 TURN 回退
rtcConfig.iceCandidatePoolSize 8 预生成候选数量,影响首帧速度
maxFPS 30 限制收流帧率,低带宽场景可用

浏览器侧的高频问题多出在布局层。比如 Vue3 后台管理系统里把像素流画布放进 iframe,父页面一个悬浮工具栏盖在播放器上方,Edge/Chrome 下鼠标事件就可能被吞掉,现象是 UE 画面正常,但点击、旋转交互没有响应。排查时先检查 iframe 容器的 pointer-events 以及父层是否透明覆盖,而不是先去改 UE 端蓝图事件。这类问题在数字孪生和内部管理平台里出现频率很高,记住这句话能省半天调试时间。

4. Vue3 与 Ue5 双向消息:从蓝图接口到事件总线

4.1 像素流的消息通道与帧数据是分开的

不少人会以为前端往 UE 发指令是某种“虚拟按键”在驱动,其实媒体流和消息通道在 WebRTC 里是两条独立链路:媒体走 RTP,消息走 DataChannel,两者在同一 PeerConnection 上维护,互不阻塞。好处是消息不占编码带宽,发送频率再高也不会掉帧;反过来,画面卡顿也不该归咎于消息发送。

UE 端接收消息的入口在像素流插件暴露的蓝图接口里。不需要自己写网络层,打开关卡蓝图,在 Pixel Streaming 分类下能找到接收消息的节点,插件版本不同节点名会有差异,但本质都是“收到一个字符串”。拿到字符串之后,剩下的就是普通蓝图开发。

4.2 UE 侧:用蓝图接口做 JSON 分发

像素流消息在浏览器和 UE 之间传递时是字符串,UE 蓝图不会自动解析业务结构。所以项目设计时要约定消息格式,我一般统一用 JSON:

{"type":"toggleCamera","payload":{"target":"main"}}

UE 侧在关卡蓝图里收到消息后,先转成 JSON 对象,读 type 字段再走分支。结构化的好处是业务扩展时只需要新增 type ,不用改蓝图的消息结构;如果不做结构化,以后每加一个参数就要重连一遍蓝图,维护成本高得多。

UE 往浏览器发送同理。在像素流蓝图接口里找发送字符串的节点,把要推给前端的数据拼成 JSON:

{"type":"connectionState","payload":{"status":"ready"}}

前端收到后按 type 分发到对应回调,组件自行消费。这套流程在纯蓝图工程里完全够用,不用写 C++。消息量特别大的场景再考虑用 C++ 函数库封装收发节点,常规交互这么做属于过度设计。

4.3 Vue3 侧封装 emitToUE 与消息分发

双向消息的 Hook 可以这样组织:

// src/composables/usePixelMessage.js
import { onBeforeUnmount, onMounted } from 'vue'

export function usePixelMessage(streaming, handlers = {}) {
  const onMessage = (event) => {
    let text = event.data
    if (typeof text !== 'string') {
      text = new TextDecoder().decode(text)
    }
    try {
      const parsed = JSON.parse(text)
      const handler = handlers[parsed.type]
      handler?.(parsed.payload)
    } catch (err) {
      console.warn('无法解析的像素流消息:', text)
    }
  }

  const emitToUE = (type, payload = {}) => {
    streaming?.sendMessage?.(JSON.stringify({ type, payload }))
  }

  onMounted(() => streaming?.addEventListener?.('message', onMessage))
  onBeforeUnmount(() => streaming?.removeEventListener?.('message', onMessage))

  return { emitToUE }
}

参数说明: handlers 是 type 到处理函数的映射,比如 { toggleCamera: fn } ,UE 发 toggleCamera 时自动触发对应逻辑; emitToUE 是前端发消息的统一出口,所有 UE 指令都走它。 TextDecoder 分支处理二进制帧,SDK 内部已经转成字符串时不会走到。 try/catch 必须有,UE 开发期经常发半截 JSON,没有容错会直接把事件链打断。

streaming 实例在组件里可以先放在 ref ,等 onMounted 之后再传给 Hook。Vue3 的响应式系统在这个场景不需要参与消息链路,消息是外部事件,用 plain object 做事件映射反而比 reactive 更清晰。

4.4 触摸、键盘与交互层的边界

官方 SDK 默认会把键盘、鼠标、触摸事件转发给 UE,桌面端几乎开箱即用。移动端要处理一个冲突:页面滚动和 UE 画布手势会打架,双指触摸场景尤其明显。UE5 默认支持双指触摸蓝图接口,但浏览器端同时也会响应 pinch 手势,两层叠加会出现画面抖动。

前端可以把手势抽象成增量消息,而不是直接转发 DOM 事件:

const onTouchMove = (e) => {
  const touch = e.touches[0]
  const dx = touch.clientX - lastX.value
  const dy = touch.clientY - lastY.value
  lastX.value = touch.clientX
  lastY.value = touch.clientY
  emitToUE('cameraMove', { dx, dy })
}

cameraMove 是自定义业务消息,和 UE 侧蓝图里的 type 一一对应; dx/dy 是两次 touchmove 之间的位移增量,UE 端按增量移动摄像机,比绝对坐标平滑。注意这里没有调用 preventDefault() ,是否阻止页面滚动取决于业务:全屏查看场景时应该阻止,内嵌在管理系统里时保留滚动反而更自然。

5. 用 chrome://webrtc-internals 验证整条像素流链路

像素流项目上线前一定要做的验证,是打开 chrome://webrtc-internals 观察连接状态。这个工具对 WebRTC 内部数据完全可见,比在 Vue 代码里到处打日志有效。连接到像素流页面后,列表里会出现对应的 RTCPeerConnection,点进去看三类数据。

数据项 正常特征 异常特征
candidate-pair state 为 succeeded ,类型是 host 或 srflx 只有 relay 说明走了 TURN,延迟明显增大
inbound-rtp 的 framesPerSecond 接近 UE 端设定帧率 持续个位数说明 UDP 丢包或编码瓶颈
inbound-rtp 的 jitter 小于 10ms 超过 30ms 画面会肉眼可感卡顿

如果不想手动翻统计页,可以在组件里临时挂一个采样器:

// 放在 connect() 里,在线调试时使用
const statsTimer = setInterval(async () => {
  const pc = streaming.peerConnection ?? streaming.pc // 字段名以 SDK 版本为准
  const report = await pc.getStats()
  report.forEach((item) => {
    if (item.type === 'inbound-rtp' && item.kind === 'video') {
      console.log('fps=', item.framesPerSecond,
                  'jitter=', item.jitter,
                  'lost=', item.packetsLost)
    }
  })
}, 2000)

逻辑说明: getStats() 返回 report 是一个 Map,遍历后按 type 过滤出入站视频流; framesPerSecond 是解码端帧率, jitter 是抖动, packetsLost 是累计丢包。如果帧率低但抖动不高,问题大概率在 UE 侧编码,比如显卡编码器把码率压到了临界值;如果帧率正常但丢包持续增长,则优先查 UDP 端口:

nc -vuz <UE服务器IP> 8888

-v 输出详细信息, -u 表示 UDP, -z 表示零字节探测,这一条只验证 UDP 端口通不通,不通就把 8888/udp 加进防火墙。像素流链路里 TCP 端口只是信令通道,媒体端口打不通,前端什么错误都可能报,唯独不会直接告诉你端口不通。

排查到最后,把 webrtc-internals 里 candidate-pair 的截图发给网络维护的人,他一眼能看出链路是局域网直连、NAT 打洞成功还是 TURN 中继,比转发十行前端报错信息直观得多。这条路跑通,像素流从 UE 到 Vue3 的整条链就真正闭环了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐