UnityRenderStreaming多对多丐中丐方案
本文介绍了一种基于WebRTC的实时流媒体传输方案,主要包含以下要点:1)采用前后端分离架构,前端通过WebAPI获取connectionId并启动Unity客户端;2)利用阿里云服务器和本地高性能主机组成混合部署环境,通过Tailscale实现内网穿透;3)详细说明了WebAPI接口实现、跨域处理、ICE服务器配置等关键技术细节;4)重点描述了connectionId的生成传递机制,确保Unit
·
流程概括如下
1、前端调WebApi 获取 connectionId,拉起Unity端传入connectionId
2、前端使用connectionId创建renderstreaming
3、Unity端仅处理指定的connectionId的消息
部署条件如下
1、云端 3M带宽双核2G内存无显卡的阿里云服务器一台
2、内网 i7-8700K 1070显卡 24GB内存Win11主机一台
丐中丐细节
1、云端运行coturn用于转发,Unity程序跑在本地内网
2、云端和本地安装Tailscale,WebAPI运行在本地内网,云端转发WebAPI端口请求到本地内网
一些具体细节
1、WebAPI接口的简单实现
[HttpGet]
[Route("/api/getConnectionId")]
public IActionResult getConnectionId()
{
string exePath = @Environment.CurrentDirectory + "/Client/Client.exe";
if (!System.IO.File.Exists(exePath))
{
Console.WriteLine(exePath + " Client.exe not found");
return NotFound(CreatResult(404, "No Client.exe file"));
}
Guid guid = Guid.NewGuid();
string connectionId = guid.ToString().ToLower();
Process process = new Process();
process.StartInfo.FileName = exePath;
process.StartInfo.Arguments = connectionId;
process.Start();
Program.dataContext.AddCon(connectionId, process.Id);
return Ok(CreatResult(200, connectionId));
}
2、设置转发消息和解决跨域请求问题
netsh interface portproxy add v4tov4 listenport=8081 listenaddress=0.0.0.0 connectport=8081 connectaddress=100.92.126.2
using Web.Models;
namespace Web
{
public class Program
{
public static DataContext dataContext;
public static void Main(string[] args)
{
Environment.CurrentDirectory = AppContext.BaseDirectory;
dataContext = new DataContext();
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://*:8081");
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin() // 允许任意源(生产环境建议指定域名)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
//if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("AllowAll"); // 👈 关键:启用 CORS 策略
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
3、前端config.js中iceServers的修改
export function getRTCConfiguration() {
let config = {};
config.sdpSemantics = 'unified-plan';
//config.iceServers = getServers();
config.iceServers = [{
urls: ['stun:x.x.x.x:19302']
}, {
urls: ['turn:x.x.x.x:3478?transport=tcp'],
username: 'xx',
credential: 'xxx'
}
];
return config;
}
4、前端main.js中获取并使用connectionId创建renderstreaming连接
async function setupRenderStreaming() {
codecPreferences.disabled = true;
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration();
renderstreaming = new RenderStreaming(signaling, config);
renderstreaming.onConnect = onConnect;
renderstreaming.onDisconnect = onDisconnect;
renderstreaming.onTrackEvent = (data) => videoPlayer.addTrack(data.track);
renderstreaming.onGotOffer = setCodecPreferences;
await renderstreaming.start();
//const urlParams = new URLSearchParams(window.location.search);
//const connectionId = urlParams.get('connectionId');
//connectionId
let connectionId = null;
try {
// 从你的 API 获取 connectionId(实际在 msg 字段中)
const resp = await fetch('http://x.x.x.x:8081/api/getConnectionId');
if (!resp.ok) {
throw new Error(`HTTP error! status: ${resp.status}`);
}
const data = await resp.json();
if (data.code !== 200 || !data.msg || typeof data.msg !== 'string') {
throw new Error('Invalid API response: missing or invalid connectionId in msg');
}
connectionId = data.msg.trim();
console.error('connectionId:', connectionId);
if (!connectionId) {
throw new Error('Empty connectionId received');
}
} catch (err) {
console.error('Failed to fetch connectionId:', err);
}
await renderstreaming.createConnection(connectionId);
}
5、Unity端获取并仅处理指定的connectionId的消息
List<string> arguments = new List<string>(Environment.GetCommandLineArgs());
// 通常第一个参数是.exe文件本身,所以这里我们只检查是否有额外的参数
if (arguments.Count <= 1)
{
Debug.LogError("没有获取到启动参数");
Application.Quit();
}
else
{
// 参数索引从1开始,因为0通常是可执行文件路径
string args = "";
for (int i = 1; i < arguments.Count; i++)
{
args += arguments[i] + " ";
}
Debug.Log("启动参数:" + args);
connectionId = args.Trim();
}
void OnCreateConnection(ISignaling signaling, string connectionId, bool polite)
{
if (Application.platform != RuntimePlatform.WindowsEditor)
{
if (this.connectionId != connectionId)
{
Debug.LogWarning("this.connectionId:" + this.connectionId
+ " 不处理其他connectionId:" + connectionId);
return;
}
}
CreatePeerConnection(connectionId, polite);
onCreatedConnection?.Invoke(connectionId);
}
void OnDestroyConnection(ISignaling signaling, string connectionId)
{
if (Application.platform != RuntimePlatform.WindowsEditor)
{
if (this.connectionId != connectionId)
{
Debug.LogWarning("this.connectionId:" + this.connectionId
+ " 不处理其他connectionId:" + connectionId);
return;
}
Application.Quit();
}
DestroyConnection(connectionId);
}
void OnAnswer(ISignaling signaling, DescData e)
{
if (Application.platform != RuntimePlatform.WindowsEditor)
{
if (this.connectionId != e.connectionId)
{
Debug.LogWarning("this.connectionId:" + this.connectionId
+ " 不处理其他connectionId:" + connectionId);
return;
}
}
if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
{
RenderStreaming.Logger.Log(LogType.Warning, $"connectionId:{e.connectionId}, peerConnection not exist");
return;
}
RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Answer, sdp = e.sdp };
_startCoroutine(pc.OnGotDescription(description, () => onGotAnswer?.Invoke(e.connectionId, e.sdp)));
}
void OnIceCandidate(ISignaling signaling, CandidateData e)
{
if (Application.platform != RuntimePlatform.WindowsEditor)
{
if (this.connectionId != e.connectionId)
{
Debug.LogWarning("this.connectionId:" + this.connectionId
+ " 不处理其他connectionId:" + e.connectionId);
return;
}
}
if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
{
return;
}
RTCIceCandidateInit option = new RTCIceCandidateInit
{
candidate = e.candidate,
sdpMLineIndex = e.sdpMLineIndex,
sdpMid = e.sdpMid
};
pc.OnGotIceCandidate(new RTCIceCandidate(option));
}
void OnOffer(ISignaling signaling, DescData e)
{
if (Application.platform != RuntimePlatform.WindowsEditor)
{
if (this.connectionId != e.connectionId)
{
Debug.LogWarning("this.connectionId:" + this.connectionId
+ " 不处理其他connectionId:" + e.connectionId);
return;
}
}
var connectionId = e.connectionId;
if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc))
{
pc = CreatePeerConnection(connectionId, e.polite);
}
RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Offer, sdp = e.sdp };
_startCoroutine(pc.OnGotDescription(description, () => onGotOffer?.Invoke(connectionId, e.sdp)));
}
来点截图


火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)