流程概括如下

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)));
}

来点截图

在这里插入图片描述
在这里插入图片描述

Logo

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

更多推荐