简介:面向需要实现远程画面传输与跨端媒体分发的Unity开发者,这份压缩包提供了一个以WebRTC为核心的信令与媒体流示例工程。场景testScene中已集成远程投屏所需的基础组件,同时引入官方WebRTC相关样例,打开即可快速体验从Unity窗口到浏览器页面的实时画面预览,是学习远程媒体流开发的极简案例。资源共包含2000个文件,压缩包大小约210.99MB。其中md和txt文档用于阅读配置说明与学习记录,json、asset、meta等文件承担工程配置与资源管理,cs、unity、prefab则对应核心脚本、场景与预制体,整体结构清晰,便于按模块查阅。已有454人学习/浏览这套源码。借助自带的Web Server通信设置与端口配置,可直接启动并验证投屏流程,省去从零搭建环境的麻烦;同时还可结合配套博文进一步了解从0到1构建可同步场景的方法,适合入门WebRTC远程投屏或在此基础上扩展功能。

1. 远程投屏项目拆解:为什么说这是 Unity WebRTC 里最省事的起点

接手过 Unity 远程调试需求的人都知道,传统方案绕不开三件事:推流 SDK 集成、转码服务器部署、播放器定制。每一件都够写两轮周报。而这个项目把整个链路压缩成了一个 testScene:Unity 侧推视频流,浏览器侧收流显示,中间只有一层本地 Web Server 做信令交换。没有商业 SDK,没有云端网关,纯 WebRTC 标准协议打通,打开工程就能跑。

它解决的核心问题是:Unity 画面如何在不开屏幕录制、不装远程桌面软件的前提下,被浏览器实时接收。对做数字孪生、设备远程运维、Unity 内容展示站的人来说,这套机制直接省掉一架推流服务器。项目自带 WebRTC Sample 和完整信令代码,适合刚接触 WebRTC 的 Unity 开发者当活体文档读,也适合要快速出 Demo 的团队直接改参数用。阅读本文大约需要 15 分钟,看完能完整说清楚信令交换、媒体协商和排错路径。

2. Unity 侧媒体流管线:从 Camera 到 WebRTC 视频轨道

2.1 testScene 里到底放了什么组件

打开 testScene,场景里的核心对象只有三个:一个主相机、一个 WebRTC 初始化的空物体、一个挂载脚本的控制节点。和很多人预想的不同,这里没有 RenderTexture 中转,也没有硬编码的推流地址,视频源直接绑定在相机上。这是 Unity WebRTC 包 3.0 之后的标准做法:通过 Camera 的纹理数据流创建 VideoStreamTrack ,再塞进 RTCPeerConnection 。

常见的初始化流程分为四步:

using Unity.WebRTC;
using UnityEngine;

public class WebRTCSender : MonoBehaviour
{
    private RTCPeerConnection connection;
    private VideoStreamTrack videoTrack;
    private Camera sourceCamera;

    void Start()
    {
        // 1. 初始化 WebRTC 全局环境
        WebRTC.Initialize();

        // 2. 从主相机生成视频轨道
        sourceCamera = GetComponent<Camera>();
        videoTrack = sourceCamera.CaptureStreamTrack(1280, 720, 0);

        // 3. 创建 RTCPeerConnection
        connection = new RTCPeerConnection();

        // 4. 添加轨道并触发协商流程
        connection.AddTrack(videoTrack);
        connection.OnIceCandidate = candidate => SendIceCandidate(candidate);
    }
}

第一步的 WebRTC.Initialize() 会在底层启动编解码器注册和网络线程池,必须在任何连接操作之前调用。第二步中 CaptureStreamTrack(1280, 720, 0) 的三个参数分别是宽度、高度、帧率,这里的 0 表示跟随相机的默认提交分辨率,不强制锁帧。第三步创建的 RTCPeerConnection 在 Unity 线程模型下运行,回调默认走主线程,UI 更新不需要额外同步。

2.2 视频编码参数与带宽取舍

默认参数能跑通,但生产环境必须显式指定编码偏好。WebRTC 的编码协商发生在 SDP 交换阶段,Unity 侧的 RTCRtpSender 暴露了 SetCodecPreferences 接口,可以优先选择硬件编码器。

// 获取发送端并设置编码优先级
RTCRtpSender sender = connection.GetSenders().Find(s => s.Track == videoTrack);
if (sender != null)
{
    RTCRtpCodecCapability[] codecs = RTCRtpSender.GetCapabilities(TrackKind.Video).codecs;
    RTCRtpCodecCapability h264 = System.Array.Find(codecs, c => c.mimeType.Contains("H264"));
    sender.SetCodecPreferences(new[] { h264 });
}

这一段的逻辑很直白:先拿到与视频轨道绑定的发送端,再从当前设备支持的编码能力里筛出 H.264,最后把偏好写回协商参数。之所以推荐优先 H.264,是因为浏览器端对 H.264 的解码兼容性最稳,VP8 虽然开源生态好,但在部分嵌入式设备的硬解支持上不如 H.264 普及。

帧率控制方面, CaptureStreamTrack 的第三个参数并不总是生效,更可靠的做法是在编码层限流:

// 限制视频码率为 2Mbps,防止局域网外传输卡顿
RTCRtpSender sender = connection.GetSenders()[0];
sender.Parameters.encodings[0].maxBitrate = 2_000_000;
sender.Parameters.encodings[0].maxFramerate = 30;

码率参数的单位是 bps,2_000_000 即 2Mbps。这个值适合 720p 的画面在普通办公网下流畅播放,如果场景里动态画面多(比如大范围旋转相机),建议上调到 4Mbps;如果只是展示静态面板,1Mbps 就足够。注意这里的 maxFramerate 和采集帧率是两层概念,采集层决定相机实际提交频率,编码层决定网络发送频率,后者才是传输带宽的瓶颈。

2.3 生命周期与资源释放

WebRTC 在 Unity 里的一个隐藏坑是生命周期管理。很多人跑完 Demo 直接关场景,第二次进入就黑屏,原因在于 WebRTC.Dispose() 没有在 OnDestroy 里补。正确的关闭顺序应该是先关连接,再停轨道,最后释放全局资源。

void OnDestroy()
{
    if (connection != null)
    {
        connection.Close();
        connection.Dispose();
    }
    if (videoTrack != null)
    {
        videoTrack.Dispose();
    }
    WebRTC.Dispose();
}

connection.Close() 会先向对端发送 BYE 消息,这是标准的信令告别流程。 Dispose() 是上层对象释放,底层媒体线程在这之后才会回收。经常有人只调 Close() 不调 Dispose() ,短暂看没问题,但连接对象每次创建都会泄漏一块非托管内存——这个项目里把两步分开写,显然是踩过坑的。

参数 推荐值 说明
分辨率 1280x720 960x540 以下文字发虚,1920x1080 对带宽要求翻倍
码率 2~4 Mbps 720p 动态画面 2M 保底,4M 上限
帧率 30 远程操作场景 30 足够,过高增加编码延迟
H.264 配置 优先选择 在线协商失败时自动退化 VP8

3. 信令服务与 Web Server: -p 参数背后的协商机制

3.1 为什么这个项目需要一个本地 Web Server

Unity 和浏览器之间不能直接建立 WebRTC 连接,必须先交换 SDP(Session Description Protocol)和 ICE Candidate,这一过程叫信令。生产环境通常用 Socket.IO 或 MQTT 做信令服务器,但示例项目为了打开即用,直接用本地 Web Server 兼职信令。具体方式是:Web Server 托管一个静态页面,Unity 侧启动后作为 HTTP 客户端把自己的 SDP offer 发过去,浏览器打开页面后拿到 offer,再通过 HTTP 响应回传 answer,整个协商链路就算闭环了。

启动命令在摘要里提过,项目根目录下的 Web Server 支持 -p 参数改端口:

./webserver -p 8080

常见做法是默认端口 80,但 80 经常被 IIS 或 Apache 占掉,所以这个 -p 参数就是专门用来处理端口冲突的。跑起来之后,Unity 侧需要在 Project Settings 里把 Web Server 的地址指向这个端口,两块配置对齐,协商才能开始。

3.2 Unity 侧信令配置与端口逻辑

Unity 编辑器里打开 Project Settings,找到 WebRTC 相关的配置项。这个项目把通信方式写成了 HTTP,端口号默认 80,如果用 -p 8080 启动,编辑器里也要同步改。很多新手在这一步漏改一个数字,结果 Unity 一直在 127.0.0.1:80 上等响应,浏览器页面却已经开在 192.168.x.x:8080 ,两边永远碰不上。

// 信令服务器地址拼接,常见做法是运行时从配置读取
string host = "192.168.1.100";  // 本机局域网 IP
int port = 8080;                // 与 -p 参数保持一致
string signalingUrl = $"http://{host}:{port}";

注意这里不能写成 localhost 或 127.0.0.1 ,因为浏览器可能跑在另一台设备上。手机、平板、另一台电脑要访问,就必须填 Unity 所在机器的局域网 IP。同一台机器调试时用 localhost 也能通,但一旦切换到真实设备,这个地址没改就会直接卡在加载页面。

3.3 SDP 协商失败时你看到的报错

WebRTC 报错对新手最不友好的地方在于,它在浏览器 console 里的错误信息往往不是“信令没配对”,而是各种底层网络报错。热词里的 stream disconnected before completion 这一系列报错,放在 WebRTC 场景下有几种可能:

第一种是 offer/answer 没有完成交换。浏览器的 ontrack 一直不触发,随后连接因为超时断开,console 显示 connection closed 。这种排查要看 Unity 侧的日志,如果 SDP 发送后迟迟等不到 answer,基本可以确认是 Web Server 端口不通或者地址写错。

第二种是 ICE 协商失败。两端能交换 SDP 但媒体流建不起来,浏览器错误是 Failed to set remote answer sdp 。常见原因是 UDP 端口被防火墙拦截,或者局域网内禁止了非标准端口的传输。先在防火墙里放行 UDP 49152-65535 段,这个范围是 WebRTC 默认的媒体端口池。

4. 浏览器端 Receive 页面接收与验证:ICE 状态与媒体流挂载

4.1 Receive 按钮后面的 JavaScript 逻辑

项目里示例页面上的 Receive 按钮,本质上是一个信令触发器。点击之后,浏览器页面里的人会执行一段核心 JavaScript,这段代码在 WebRTC 场景里基本属于公共模板:

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

pc.ontrack = (event) => {
  const video = document.getElementById('remoteVideo');
  video.srcObject = event.streams[0];
  video.play();
};

pc.oniceconnectionstatechange = () => {
  console.log('ICE state:', pc.iceConnectionState);
};

// 发送 answer 回 Unity 侧
async function receive() {
  const offer = await fetch('/offer').then(res => res.json());
  await pc.setRemoteDescription(offer);
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  fetch('/answer', {
    method: 'POST',
    body: JSON.stringify(answer)
  });
}

这段逻辑里最关键的是两点: ontrack 里把拿到的媒体流直接绑到 <video> 元素上,这一步是将远端的相机画面落地的关键;另一个是 oniceconnectionstatechange ,它输出的状态值可以直接反映网络协商进度。跑通的话,状态会依次变成 checking → connected → completed ,中途任何一步卡住,都能在主控台上第一时间看到。

4.2 验证连接是否正常的三个维度

验证 WebRTC 远程投屏是否正常工作,不能只盯着画面能不能出来。我一般会同时看三个维度:

第一个是 ICE 状态。consle 里如果停在 checking 不动,大概率是 Candidate 交换有问题。第二看 pc.getStats() 里的视频码率:

const stats = await pc.getStats();
stats.forEach(report => {
  if (report.type === 'inbound-rtp' && report.kind === 'video') {
    console.log('codec:', report.codecId);
    console.log('bitrate:', report.bitrate);
  }
});

第三看实际延迟。拿秒表拍 Unity 画面里的计时器,再对比浏览器画面,就是这个链路的端到端延迟。Unity WebRTC 本地局域网跑 720p@30fps,端到端延迟控制在 100~200ms 属于正常水平,超过 500ms 就需要检查是不是打开了 vsync 或者编码器配置出现了异常。

4.3 常见报错速查:从双端日志定位问题

stream disconnected before completion 这个报错在 WebRTC 调试过程中经常出现,但它通常不是 WebRTC 本身的错误,而是 HTTP 请求在等待阶段被打断。配合 transport error: network error 一起出现时,优先怀疑信令 HTTP 请求超时。下面是几类高频报错的对应处理方式:

报错内容 可能原因 处理措施
stream disconnected before completion: stream closed before response.completed 信令请求发出后连接被关闭 检查 Unity 侧 Web Server 是否还在运行,端口是否被占用
transport error: network error: error decoding response body 信令响应体解析失败 确认 Unity 和浏览器的 SDP 格式一致,看是否混入了多余字符
too many pending requests, please retry 上一次协商未结束就发起了新请求 点击 Receive 前先刷新页面,确保连接状态是 new 而不是 connecting
iceConnectionState = failed ICE 候选全部失败 防火墙放行 UDP 端口,或者改用 host 候选直接走局域网直连

这里要注意一个容易误判的地方:浏览器控制台里出现 stream disconnected before completion 时,很多人第一反应是 WebRTC 媒体流断了,但实际往往是信令通道的先决请求被打断,媒体流根本没有机会建立。判断依据是看 onconnectionstatechange 是否已经被触发——如果连 new → connecting 都没走到,问题一定在 HTTP 信令层,和 RTCPeerConnection 无关。

5. 进阶应用:把连接参数从硬编码改成动态协商

项目默认的玩法是 Unity 固定推流、浏览器只能看,这在实际部署里马上会遇到一个问题:Unity 侧 IP 变了,浏览器端就得改地址。更合理的做法是让信令服务同时提供一个动态注册界面,Unity 启动时把自己的 Host 和端口上报,浏览器端通过一个固定的入口获取当前可用的推流端列表。

实现思路是把原来的静态 HTML 替换成带简单后端接口的页面。Unity 侧启动后向 Web Server 的 /register 接口提交一条记录,浏览器进入页面时先请求 /streams 拿到当前可用的推流端,再生成带参数的 URL。这样多台 Unity 设备可以同时在线,每台都是一个可以独立选择的媒体流源,远程运维和展览展示的场景就不需要为每一台设备手动改配置了。

# 简单的信令注册接口,保存当前在线推流端
from flask import Flask, jsonify, request

app = Flask(__name__)
streams = {}

@app.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    streams[data['id']] = {'host': data['host'], 'port': data['port']}
    return jsonify({'status': 'ok', 'streams': streams})

@app.route('/streams', methods=['GET'])
def list_streams():
    return jsonify(streams)

这个 30 行的接口足够替换掉原来的静态信令页面,并且保留了你已经调通的 WebRTC 全部链路。注册接口启动后,浏览器端点击 Receive 时把 streams 里对应设备的地址写进 SDP 协商,不需要改动 Unity 侧的任何代码。这套模式对同时管理多台机器画面尤其有用,比如机房巡检或者展会多点位展示,打开控制页面就能在几个推流源之间切换,比反复改 webserver -p 参数再重启省太多时间。

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

Logo

邀请您加入社区

更多推荐