Unity3D接入萤石云API实现海康大华摄像头实时视频流播放
2026/8/7 16:34:21 网站建设 项目流程

1. 项目概述与核心价值

最近在做一个智慧园区或者安防监控相关的Unity项目时,一个绕不开的需求就是如何把市面上主流的网络摄像头(比如海康、大华)的实时视频流,稳定、低延迟地接入到Unity的3D场景里。你可能想在大屏上展示一个3D园区,然后点击某个楼栋,就能弹出这个楼门口的实时监控画面;或者做一个AR巡检应用,把摄像头画面叠加在真实设备上。这个需求听起来很直接,但真动手做,你会发现坑不少:海康、大华自家的SDK虽然强大,但往往很重,而且和Unity的跨平台特性(尤其是WebGL、移动端)兼容性不佳。更常见的情况是,设备已经接入了像萤石云这样的公有云平台,你拿不到设备的局域网RTSP流,只能通过云平台的API来获取直播流。

这个项目要解决的,就是如何在Unity3D中,通过C#代码,调用萤石云的开放API,获取已接入该平台的海康或大华摄像头的直播视频流地址,并最终在Unity的UI(比如RawImage)或3D物体(比如材质球)上播放出来。我会把完整的实现思路、踩过的坑、以及可以直接拿来用的核心源码都分享出来。无论你是做数字孪生、安防可视化,还是简单的监控集成,这套方案都能给你提供一个清晰、可落地的技术路径。

2. 整体技术方案设计与选型考量

2.1 为什么选择萤石云API而非设备直连?

首先得搞清楚我们为什么舍近求远,不直接用OpenCV或者FFmpeg去拉设备的RTSP流,而要绕道云平台。这里有几个很现实的考量:

设备直连(RTSP)的局限性:

  1. 网络穿透问题:大部分安防摄像头部署在局域网或通过NAT隔离。从公网的Unity客户端(比如WebGL应用)直接访问局域网的RTSP端口(默认554)几乎不可能,需要复杂的网络配置,如端口映射、内网穿透,这在很多客户现场是行不通的。
  2. 安全性:直接暴露RTSP端口和认证信息到公网有安全风险。萤石云等平台提供了Token鉴权、流加密等安全机制。
  3. 平台兼容性:海康、大华的设备SDK(如HCNetSDK)通常提供的是C/C++或Java库,在Unity的某些平台(尤其是WebGL和iOS)上集成和编译非常困难,甚至不可行。
  4. 流媒体格式:设备原始的RTSP流可能是H.264/H.265编码,需要额外的解码库才能在Unity中渲染。而云平台往往提供了更友好的输出格式,比如FLV、HLS,甚至WebRTC。

萤石云API方案的优势:

  1. 标准化接入:萤石云为海康威视及其兼容设备提供了统一的云服务平台。通过其开放的API,我们可以用标准的HTTP/HTTPS请求,获取到经过转码、适配网络环境的直播流地址。
  2. 跨网络访问:只要设备在线,无论它在哪里,我们都能通过公网API获取到可访问的流地址,完美解决了网络穿透问题。
  3. 功能丰富:除了直播流,API还提供了设备管理、云台控制、录像回放、报警信息订阅等一系列功能,为项目扩展留下了空间。
  4. 相对轻量:我们只需要处理HTTP请求和JSON解析,无需集成庞大的设备原生SDK,项目更简洁,跨平台部署更容易。

所以,如果你的摄像头已经注册到了萤石云(这是海康设备的常见做法),或者你愿意将设备接入萤石云,那么通过其API获取视频流是最务实、最稳定的方案。

2.2 Unity端播放流媒体的技术选型

拿到流地址(通常是一个URL)后,下一步就是在Unity里把它播出来。这里有几个主流方案:

方案一:使用VideoPlayer组件 + 透明通道这是Unity原生的方案。VideoPlayer支持播放网络视频(VideoSource.Url)。你可以将获取到的直播流URL(例如萤石云提供的HLSm3u8地址)直接赋给VideoPlayer。播放的内容可以渲染到RenderTexture,再赋值给UI的RawImage或3D物体的Material

  • 优点:原生支持,无需第三方插件,对于HLS格式兼容性较好。
  • 缺点:对RTMP、FLV等格式支持依赖平台(如需要Android上集成ExoPlayer扩展),延迟相对较高,自定义控制(如抓帧、分析)能力弱。

方案二:集成FFmpeg或原生解码库通过C#调用本地FFmpeg库,或者使用诸如libvlcfor Unity的插件(如VLC for Unity),进行软解码或硬解码。

  • 优点:格式支持最全(RTSP, RTMP, FLV, HLS等),延迟可控,功能强大(可自定义解码前、后的数据处理)。
  • 缺点:集成复杂度高,库体积大,跨平台(尤其是WebGL)支持非常困难,甚至不可能。

方案三:使用专门的Unity流媒体插件市场上有一些成熟的付费插件,如AVPro Video、uWebRTC等。它们封装了底层解码和渲染逻辑。

  • 优点:开箱即用,功能强大,跨平台支持好,有技术支持。
  • 缺点:需要付费,项目成本增加。

方案四:WebView或浏览器内核嵌入对于WebGL平台,一个取巧的办法是将视频流在一个网页中播放,然后通过Unity与JavaScript的交互来控制。或者使用能嵌入浏览器内核的插件(在PC/移动端)。

  • 优点:可以充分利用浏览器强大的视频播放能力。
  • 缺点:集成复杂,性能开销大,不适合需要与3D场景深度交互的场景。

实操心得:对于大多数以展示为主的数字孪生、监控大屏项目,如果延迟要求不是极致的毫秒级(比如游戏对战),方案一(VideoPlayer + HLS)是性价比最高的选择。萤石云API恰好提供了HLS格式的流地址,与VideoPlayer是天作之合。这个方案实现简单、跨平台(iOS/Android/PC基本没问题,WebGL需注意),且完全免费。本项目也将主要围绕这个方案展开。如果你的项目对延迟有苛刻要求(<1秒),可能需要考虑方案二或三,但那将是另一个复杂得多的课题。

3. 萤石云API接入核心流程解析

3.1 前期准备:获取API密钥与设备信息

在写代码之前,你需要先在萤石云开放平台( open.ys7.com )完成以下准备工作:

  1. 注册开发者账号并创建应用:登录开放平台,创建一个应用。应用类型根据你的实际场景选择(如“自用型应用”)。创建成功后,你会获得至关重要的AppKeySecret。这两个参数相当于你调用API的账号和密码,务必妥善保管,不要泄露在客户端代码中(重要!)。
  2. 将摄像头接入萤石云:确保你的海康或大华摄像头已经添加到萤石云账户下。在萤石云App或官网中,你可以看到设备的序列号(deviceSerial,通常是一串字母数字组合)和验证码(如果有)。同时,确认设备已开启“视频分享”或相关权限。
  3. 获取设备通道号:一个摄像头可能有多通道(如主码流、子码流)。默认通道号通常是1。你可以在设备管理页面查看。

注意事项AppKeySecret是最高权限的凭证。绝对不要将它们硬编码在Unity的C#脚本里,尤其是准备打包发布到客户端(如PC、手机)的版本。任何反编译工具都能轻易提取出这些字符串,导致你的账户被盗用,产生流量费用或安全风险。正确的做法是:自己搭建一个简单的后端代理服务。Unity客户端只与你自己的服务器通信,由服务器保管Secret并代为调用萤石云API。这是生产环境必须遵守的安全规范。下文为了演示完整流程,代码中会包含这些参数,但请务必牢记这一点。

3.2 API调用链与AccessToken管理

萤石云API调用遵循OAuth 2.0的客户端凭证模式。核心流程分为两步:

第一步:获取访问令牌(AccessToken)这是所有后续API调用的“门票”。你需要使用AppKeySecret来换取一个有一定有效期的AccessToken

  • 接口POST /api/lapp/token/get
  • 参数appKey,appSecret
  • 返回:包含accessToken(有效期默认约7天)、过期时间等。

第二步:使用Token获取设备直播地址拿到AccessToken后,就可以查询指定设备的直播流地址了。

  • 接口POST /api/lapp/v2/live/address/get
  • 参数accessToken,deviceSerial(设备序列号),channelNo(通道号,默认为1),protocol(流协议,如rtmp,hls,flv),quality(视频质量,如2代表高清)
  • 返回:包含url(直播地址)、expireTime(地址过期时间)等。

Token的管理策略: 由于Token有有效期,我们不能每次播放视频都去申请一次。一个合理的策略是:

  1. 在Unity客户端启动时,向自己的后端服务器请求一个可用的直播地址(后端服务器会管理Token的获取与刷新)。
  2. 或者,客户端首次请求时,后端返回Token和地址,客户端在内存中缓存这个地址,直到播放失败(可能因为地址过期),再重新向后端请求。

在下面的源码实现中,为了保持逻辑完整,我们将演示一个不安全的、仅供学习测试的客户端直连版本。它会直接在C#里调用萤石云API。再次强调,产品化时请务必改为通过你自己的后端服务器中转。

4. Unity C# 核心源码实现与详解

接下来,我们创建一个Unity C#脚本,命名为EzvizStreamFetcher.cs。这个脚本将负责与萤石云API交互,并控制VideoPlayer播放。

4.1 定义数据结构与常量

首先,定义与API返回格式对应的数据结构,以及所需的常量。

using System; using UnityEngine; using UnityEngine.Networking; // 用于UnityWebRequest using UnityEngine.Video; using System.Collections; using System.Text; using System.Collections.Generic; [System.Serializable] public class EzvizTokenResponse { public string code; // 状态码, "200"表示成功 public string msg; // 消息 public EzvizTokenData data; } [System.Serializable] public class EzvizTokenData { public string accessToken; // 访问令牌 public long expireTime; // 过期时间戳(毫秒) } [System.Serializable] public class EzvizLiveAddressResponse { public string code; public string msg; public EzvizLiveAddressData data; } [System.Serializable] public class EzvizLiveAddressData { public string url; // 直播流地址,例如 https://hls.open.ys7.com/openlive/xxxxxx.m3u8 public long expireTime; public int delayTime; // 延迟时间 } public class EzvizStreamFetcher : MonoBehaviour { // 【警告】以下参数在生产环境中必须放在服务端! public string appKey = "你的AppKey"; public string appSecret = "你的AppSecret"; public string deviceSerial = "你的设备序列号"; public int channelNo = 1; // 通道号 public string protocol = "hls"; // 推荐使用hls,兼容性好 public int quality = 2; // 2-高清,1-流畅 private string currentAccessToken = ""; private long tokenExpireTime = 0; private VideoPlayer videoPlayer; private RenderTexture outputTexture; // 萤石云API地址 private const string API_BASE_URL = "https://open.ys7.com/api/lapp/"; }

代码解析:这里定义了与萤石云API返回的JSON格式完全匹配的类结构。使用[System.Serializable]属性是为了方便Unity的JsonUtility进行序列化和反序列化。UnityWebRequest是Unity推荐的网络请求工具,支持跨平台。我们将使用VideoPlayer组件进行播放。

4.2 实现AccessToken获取方法

这个方法负责调用第一个API,获取AccessToken。我们会将其缓存起来。

private IEnumerator GetAccessTokenAsync(System.Action<string> onSuccess, System.Action<string> onFailure) { string url = API_BASE_URL + "token/get"; WWWForm form = new WWWForm(); form.AddField("appKey", appKey); form.AddField("appSecret", appSecret); using (UnityWebRequest request = UnityWebRequest.Post(url, form)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { string jsonResponse = request.downloadHandler.text; EzvizTokenResponse response = JsonUtility.FromJson<EzvizTokenResponse>(jsonResponse); if (response.code == "200") { currentAccessToken = response.data.accessToken; tokenExpireTime = response.data.expireTime; Debug.Log($"AccessToken 获取成功: {currentAccessToken.Substring(0, 20)}..., 过期时间: {UnixTimeStampToDateTime(tokenExpireTime)}"); onSuccess?.Invoke(currentAccessToken); } else { Debug.LogError($"获取AccessToken失败: {response.msg} (代码: {response.code})"); onFailure?.Invoke($"API错误: {response.msg}"); } } else { Debug.LogError($"网络请求失败: {request.error}"); onFailure?.Invoke($"网络错误: {request.error}"); } } } // 辅助方法:将Unix时间戳(毫秒)转换为DateTime private DateTime UnixTimeStampToDateTime(long unixTimeStampMillis) { System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); dtDateTime = dtDateTime.AddMilliseconds(unixTimeStampMillis).ToLocalTime(); return dtDateTime; }

实操要点:这里使用了WWWForm来构建表单提交的POST请求。注意,UnityWebRequest在完成使用后,最好包裹在using语句中或手动调用Dispose(),以释放网络资源,这是一个好习惯。成功获取Token后,我们将其存储在成员变量中,并记录过期时间。在实际项目中,你应该将这个时间与当前时间对比,在Token即将过期前主动刷新。

4.3 实现直播流地址获取方法

有了Token,我们就可以获取最终的视频流播放地址了。

private IEnumerator GetLiveStreamUrlAsync(string accessToken, System.Action<string> onSuccess, System.Action<string> onFailure) { // 检查Token是否有效(简单检查) if (string.IsNullOrEmpty(accessToken)) { onFailure?.Invoke("AccessToken无效"); yield break; } string url = API_BASE_URL + "v2/live/address/get"; WWWForm form = new WWWForm(); form.AddField("accessToken", accessToken); form.AddField("deviceSerial", deviceSerial); form.AddField("channelNo", channelNo); form.AddField("protocol", protocol); form.AddField("quality", quality); using (UnityWebRequest request = UnityWebRequest.Post(url, form)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { string jsonResponse = request.downloadHandler.text; EzvizLiveAddressResponse response = JsonUtility.FromJson<EzvizLiveAddressResponse>(jsonResponse); if (response.code == "200") { string liveUrl = response.data.url; Debug.Log($"直播流地址获取成功: {liveUrl}"); onSuccess?.Invoke(liveUrl); } else { Debug.LogError($"获取直播地址失败: {response.msg} (代码: {response.code})"); // 常见错误:Token过期(错误码 10002) if (response.code == "10002") { Debug.Log("AccessToken可能已过期,尝试重新获取..."); // 这里可以触发Token刷新逻辑 } onFailure?.Invoke($"API错误: {response.msg}"); } } else { Debug.LogError($"网络请求失败: {request.error}"); onFailure?.Invoke($"网络错误: {request.error}"); } } }

关键点解析:这个方法接收上一步获取的accessToken作为参数。注意我们传入了protocol参数,这里指定为”hls”,因为我们要用Unity的VideoPlayer播放。如果获取失败,并且错误码是”10002”,这通常意味着Token过期,在实际逻辑中应该触发重新获取Token的流程。

4.4 整合流程与VideoPlayer播放控制

现在,我们将上述步骤串联起来,并初始化VideoPlayer来播放获取到的URL。

void Start() { // 初始化VideoPlayer组件 videoPlayer = gameObject.AddComponent<VideoPlayer>(); videoPlayer.playOnAwake = false; videoPlayer.waitForFirstFrame = true; // 等待第一帧加载 videoPlayer.skipOnDrop = true; // 允许丢帧以追赶实时流 videoPlayer.source = VideoSource.Url; // 创建RenderTexture用于视频输出 outputTexture = new RenderTexture(1920, 1080, 24); videoPlayer.targetTexture = outputTexture; // 开始获取视频流的流程 StartCoroutine(FetchAndPlayStream()); } private IEnumerator FetchAndPlayStream() { Debug.Log("开始获取萤石云直播流..."); string token = null; bool tokenSuccess = false; // 步骤1:获取AccessToken yield return StartCoroutine(GetAccessTokenAsync( (accessToken) => { token = accessToken; tokenSuccess = true; }, (error) => { Debug.LogError($"获取Token失败: {error}"); } )); if (!tokenSuccess) yield break; string streamUrl = null; bool urlSuccess = false; // 步骤2:使用Token获取直播地址 yield return StartCoroutine(GetLiveStreamUrlAsync(token, (url) => { streamUrl = url; urlSuccess = true; }, (error) => { Debug.LogError($"获取直播地址失败: {error}"); } )); if (!urlSuccess) yield break; // 步骤3:使用VideoPlayer播放 PlayVideo(streamUrl); } private void PlayVideo(string url) { if (videoPlayer == null) return; videoPlayer.url = url; videoPlayer.Prepare(); // 准备视频 // 监听准备完成事件 videoPlayer.prepareCompleted += (VideoPlayer source) => { Debug.Log($"视频准备就绪,开始播放。分辨率: {source.width}x{source.height}"); source.Play(); // 将RenderTexture赋值给一个RawImage或Material // 例如:GetComponent<RawImage>().texture = outputTexture; }; // 监听错误事件 videoPlayer.errorReceived += (VideoPlayer source, string message) => { Debug.LogError($"视频播放出错: {message}"); // 可以在这里加入重试逻辑,比如重新获取流地址 }; } void OnDestroy() { if (videoPlayer != null) { videoPlayer.Stop(); } if (outputTexture != null) { outputTexture.Release(); } }

播放设置详解

  • waitForFirstFrame = true: 对于网络流,等待第一帧加载完成再开始播放,可以避免黑屏或初始卡顿。
  • skipOnDrop = true: HLS是流式传输,网络波动可能导致缓冲。开启此选项允许播放器在缓冲不足时跳过一些帧以保持实时性,这对直播场景很重要。
  • Prepare(): 这是一个异步操作,不会阻塞主线程。我们需要监听prepareCompleted事件来知道何时可以开始播放。
  • errorReceived: 网络不稳定、流地址失效等情况都会触发此事件,这是进行错误处理和重试的关键入口。

4.5 在UI或3D物体上显示视频

最后一步是将VideoPlayer输出的RenderTexture显示出来。

在UI上显示(如RawImage):

  1. 在Canvas上创建一个RawImage组件。
  2. 将脚本挂载到任意GameObject上,并配置好参数。
  3. PlayVideo方法的prepareCompleted事件回调中,添加一行代码:
    // 假设你有一个public RawImage targetRawImage的引用 if (targetRawImage != null) { targetRawImage.texture = outputTexture; }

在3D物体上显示(如材质球):

  1. 创建一个3D物体(如Quad或Plane)。
  2. 为其创建一个新的材质(Material),Shader选择Unlit/TextureStandard(需要调整)。
  3. 在脚本中,将这个材质的Main Texture赋值为outputTexture
    public Renderer targetRenderer; // 拖拽你的3D物体的Renderer组件到此 ... videoPlayer.prepareCompleted += (VideoPlayer source) => { source.Play(); if (targetRenderer != null) { targetRenderer.material.mainTexture = outputTexture; } };

5. 常见问题、排查技巧与优化建议

在实际部署和测试中,你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方案整理出来。

5.1 网络与API调用问题

问题1:UnityWebRequest报错 “Cannot connect to destination host”

  • 排查:这通常是网络连通性问题。首先检查Unity编辑器或打包后的应用是否能正常访问互联网。其次,检查萤石云API地址open.ys7.com是否被防火墙或网络策略屏蔽(在某些企业内网可能出现)。
  • 解决:尝试在浏览器中直接访问https://open.ys7.com,看是否能打开。如果不行,需要配置网络代理或调整防火墙规则。

问题2:API返回错误码 “10002” (非法访问令牌) 或 “10005” (accessToken过期)

  • 排查:这是最常见的问题。说明你使用的AccessToken无效或已过期。
  • 解决
    1. 实现Token刷新机制:在发起获取直播地址的请求前,检查本地保存的tokenExpireTime是否已接近或超过当前时间。如果是,则先调用GetAccessTokenAsync获取新的Token。
    2. 错误重试:在GetLiveStreamUrlAsync的失败回调中,如果识别到错误码是10002或10005,则自动触发一次重新获取Token并重试获取流地址的流程。

问题3:API返回错误码 “20002” (设备不存在) 或 “20032” (设备不在线)

  • 排查deviceSerial(设备序列号)填写错误,或者设备确实没有接入萤石云,或设备当前离线。
  • 解决:登录萤石云官网或App,仔细核对设备的序列号。确保设备电源和网络连接正常,在萤石云平台上显示为在线状态。

5.2 视频播放与渲染问题

问题4:VideoPlayer一直处于Preparing状态,不开始播放

  • 排查:HLSm3u8地址可能无效,或者网络环境无法流畅加载流媒体数据。也可能是VideoPlayer对该格式的支持问题。
  • 解决
    1. 将获取到的streamUrl复制到电脑的VLC播放器中测试,看是否能播放。如果不能,说明流地址本身有问题,需要检查API调用参数(如protocol,quality)。
    2. 在Unity中,检查VideoPlayererrorReceived事件,看是否有具体的错误信息。
    3. 尝试降低清晰度(将quality设为1),或者更换protocolflv(如果VideoPlayer目标平台支持)试试。

问题5:视频播放卡顿、延迟高(十几秒以上)

  • 排查:这是HLS协议的固有特性。HLS为了保障流畅性,会将视频切片传输,通常会有10-30秒的延迟,不适合需要实时交互的场景。
  • 解决
    1. 接受延迟:如果只是用于监控查看,这个延迟通常可以接受。
    2. 寻求低延迟方案:萤石云API也支持rtmpflv协议,延迟可以降到3-5秒。但Unity原生的VideoPlayer在大部分平台上不支持直接播放RTMP/FLV。你需要集成第三方插件,如AVPro Video(付费,支持好)或尝试使用FFmpeg库进行解码(复杂,跨平台坑多)。
    3. 使用WebRTC(终极方案):萤石云部分高端设备和支持WebRTC协议的接入服务。WebRTC可以实现亚秒级延迟。但这需要在Unity中集成WebRTC库(如Unity官方WebRTC包),并与萤石云的WebRTC信令服务器对接,复杂度最高。

问题6:在UI上显示视频,但画面扭曲或比例不对

  • 排查RawImage的RectTransform尺寸或RenderTexture的尺寸与视频源分辨率不匹配。
  • 解决
    1. videoPlayer.prepareCompleted事件中,可以获取到视频的原始宽高(source.width,source.height)。
    2. 根据宽高比,动态调整RawImage所在RectTransform的尺寸,或者调整RawImageuvRect来保持比例。
    3. 也可以创建RenderTexture时,使用视频的原始分辨率(但可能性能开销大)。

5.3 安全与架构优化建议

安全加固(必须做): 如前所述,将AppKeySecret放在客户端是极度危险的。请务必搭建一个轻量级后端服务(可以用Node.js, Python Flask, C# ASP.NET Core等任何你熟悉的技术)。

  • 客户端:只向你自己的服务器发送请求,例如GET /api/camera/stream?deviceId=xxx
  • 服务器:接收请求后,用保存在服务器环境变量或配置文件中的AppKeySecret去调用萤石云API,获取流地址,然后返回给客户端。同时,服务器端可以实现Token的缓存和自动刷新,效率更高。

性能与体验优化

  1. 预加载与缓存:在场景加载时,就提前开始获取Token和流地址,而不是等用户点击时才进行,减少等待时间。
  2. 多个摄像头管理:如果需要同时播放多个摄像头,不要为每个摄像头都创建独立的VideoPlayer组件并同时Prepare。这会导致巨大的网络和CPU开销。应该实现一个视频流管理池,按需加载和播放。
  3. 错误重试与状态提示:网络请求和视频播放都可能失败。设计良好的重试机制(如指数退避)和用户友好的加载中、错误提示界面,能极大提升产品体验。
  4. 释放资源:在不需要播放时(如切换场景、关闭窗口),务必调用videoPlayer.Stop()并释放RenderTextureoutputTexture.Release()),如OnDestroy方法中所做,防止内存泄漏。

这套从萤石云API调用到Unity内播放的完整链路,我已经在多个数字孪生项目中实际应用过。核心难点不在于代码本身,而在于对网络流媒体协议的理解、跨平台兼容性的把握,以及生产环境下的安全架构设计。希望这份详细的拆解和源码能帮你避开我当年踩过的那些坑,顺利把摄像头视频流搬进你的Unity世界。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询