Unity接入海康UMP流全流程:签名认证、HTTP长连接与自定义渲染

发布时间:2026/5/22 21:30:51

Unity接入海康UMP流全流程:签名认证、HTTP长连接与自定义渲染 1. 这不是简单的“拉流”而是一场跨协议、跨权限、跨引擎的精准对接你有没有试过在Unity里直接填一个RTSP地址比如rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101然后点播放——结果黑屏、报错、卡死或者更糟什么反应都没有我试过不下二十次。不是Unity不支持RTSP而是海康的设备早就不让你这么“裸连”了。从2021年固件升级开始所有新出厂的DS-2CD、DS-2TD系列IPC/NVR只要开启HTTPS或平台接入功能RTSP流默认强制启用签名认证Signature Authentication。它不像传统HTTP Basic Auth那样把账号密码明文塞进URL而是要求你在请求头里带上一串动态生成的、有时效性的Authorization签名。这一步跳过去Unity的VideoPlayer组件连TCP三次握手都通不过更别说解码播放了。这个标题里的“全流程”指的就是绕不开的三道硬门槛第一道是签名生成——你得理解海康Hikvision私有签名算法基于HMAC-SHA256 时间戳 随机数 URI路径拼接不能靠Postman随便点点就完事第二道是UMP协议适配——海康早已不推荐原生RTSP而是主推其自研的UMPUnified Media Protocol流它本质是RTSP over HTTPTLS封装但兼容性、稳定性、低延迟表现远超传统RTSPUnity必须通过特定插件或自定义网络层才能识别第三道是Unity播放链路重构——VideoPlayer原生只吃本地文件或HTTP-FLV/HLS对带签名的UMP流完全无感你得用RenderTexture做中转用WebCamTexture思路重写帧捕获逻辑再喂给RawImage或Shader。这不是调个API的事这是把Unity当成一个轻量级媒体网关来用。适合谁适合正在做安防可视化大屏、智慧工地AR巡检、数字孪生IOC系统的Unity开发者尤其是那些被甲方一句“必须接海康摄像头”堵在项目门口的人。本文不讲SDK下载链接不贴几行“能跑”的Demo代码而是带你亲手拆开签名怎么算、UMP怎么发、Unity怎么“骗过”自己的播放器——每一步都有实测参数、失败日志截图文字还原、以及我踩出的四个血坑。2. 签名生成不是Base64加密而是HMAC-SHA256的三段式精密拼接2.1 海康签名机制的本质一次HTTP请求的“数字门禁卡”很多人误以为海康签名是“把密码加密一下”其实它和JWT签名逻辑高度相似但更轻量、更依赖服务端时间同步。它的核心不是保护密码而是防重放攻击Replay Attack和防URL篡改。当你向海康设备发起RTSP流请求时实际是UMP的HTTP GET请求设备会校验三个关键字段timeStamp当前UTC毫秒时间戳非本地时间必须和服务端误差30秒random32位随机字符串a-z, A-Z, 0-9每次请求必须不同uri请求路径的规范化URI不含域名、协议、查询参数仅/artemis/api/video/startStream这类。这三者按固定顺序拼成原始字符串再用设备的AccessKeySecret不是密码是设备Web页面“平台接入”里生成的密钥做HMAC-SHA256哈希最后Base64编码——这才是真正的Authorization头值。整个过程没有“加密”只有“签名”设备端用同样的AccessKeySecret和收到的timeStamp/random/uri复算一遍比对结果是否一致。一旦timeStamp偏差超30秒或random重复签名立刻失效。提示AccessKeySecret在海康iVMS-4200或Web管理界面“配置 网络 平台接入”中生成不是设备登录密码也不是ONVIF密码。首次启用平台接入时系统自动生成一对AccessKeyID/AccessKeySecret后者必须严格保密。我曾因把AccessKeySecret硬编码进Unity AssetBundle被甲方安全审计打回教训是必须走运行时密钥分发服务或至少AES-256本地加密存储。2.2 手动计算签名的完整步骤与C#实现Unity可直接复用我们以实际UMP流启动请求为例目标设备IP192.168.1.64端口443HTTPS请求路径/artemis/api/video/startStreamAccessKeyIDAKIAIOSFODNN7EXAMPLEAccessKeySecretwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY均为示例真实环境请替换。第一步构造规范化URI必须严格小写去除所有查询参数和锚点开头带//artemis/api/video/startStream第二步生成timeStamp与random// C# Unity脚本片段需using System.Security.Cryptography; long timeStamp DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // 注意必须UTC非DateTime.Now string random Path.GetRandomFileName().Replace(., ).Substring(0, 32); // 确保32位字母数字注意DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()返回的是毫秒级时间戳海康要求精确到毫秒。我曾用DateTime.Now.ToString(yyyyMMddHHmmss)导致签名永远失败——因为那是字符串格式不是数值时间戳。第三步拼接待签名字符串SIGN_STRING格式为timeStamp\nrandom\nuri注意\n是换行符不是字符串\n1717023456789 aBcDeFgHiJkLmNoPqRsTuVwXyZ12345 /artemis/api/video/startStream第四步HMAC-SHA256哈希 Base64编码string signString ${timeStamp}\n{random}\n{/artemis/api/video/startStream}; byte[] keyBytes Encoding.UTF8.GetBytes(wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY); byte[] dataBytes Encoding.UTF8.GetBytes(signString); using (var hmac new HMACSHA256(keyBytes)) { byte[] hashBytes hmac.ComputeHash(dataBytes); string signature Convert.ToBase64String(hashBytes); // 最终Authorization头值 string authHeader $HMAC-SHA256 AccessKeyIdAKIAIOSFODNN7EXAMPLE, Signature{signature}, time{timeStamp}, random{random}; }第五步组装完整HTTP请求头GET /artemis/api/video/startStream?channel101streamType0protocolUDPtransmodeUDPformatPS HTTP/1.1 Host: 192.168.1.64:443 Authorization: HMAC-SHA256 AccessKeyIdAKIAIOSFODNN7EXAMPLE, Signaturebase64_encoded_signature, time1717023456789, randomaBcDeFgHiJkLmNoPqRsTuVwXyZ12345 Content-Type: application/json实操心得我在测试时发现如果Content-Type没设成application/json即使签名正确设备也返回400 Bad Request。海康文档没写这点但抓包对比iVMS-4200的请求头它确实带了这个头。另外streamType0代表主码流1是子码流protocolUDP是海康推荐的低延迟模式但Unity在弱网下建议切TCP需改protocolTCP并加?timeout30参数。2.3 签名验证失败的四大高频原因与排查链路错误现象根本原因排查方法我的修复方案401 UnauthorizedtimeStamp本地时间与设备UTC时间偏差30秒用curl -v https://192.168.1.64/doc/page/login.asp看响应头Date:字段对比本地date -u在Unity启动时调用NTP服务器如time.windows.com校准不用系统时间403 Forbiddenrandom字符串含非法字符如,/,或长度≠32日志打印random变量用正则^[a-zA-Z0-9]{32}$校验改用System.Security.Cryptography.RandomNumberGenerator生成字节数组再Base32编码400 Bad Requesturi拼写错误如多/、少/、大小写混用或Content-Type缺失抓包对比iVMS-4200请求用Wireshark过滤http.request.uri contains startStream写单元测试对/artemis/api/video/startStream等所有URI路径做硬编码常量500 Internal ErrorAccessKeySecret被重置或设备未启用平台接入登录Web界面检查“平台接入”开关状态确认AccessKey未过期设备端重新生成AccessKey并在Unity配置表中热更新避免重启App我踩过的最深的坑是某次设备固件升级后平台接入默认关闭但Web界面没提示。我对着正确的签名调试了三天最后发现设备压根没开签名验证——它直接走传统Basic Auth但Unity VideoPlayer又不支持带符号的URL自动解析。解决方案写个预检接口先GET/artemis/api/platform/v1/devices?deviceCodexxx看返回是否含platformAccess: true字段再决定走签名还是传统流。3. UMP流获取不是RTSP URL而是HTTP长连接的JSON信令交互3.1 为什么必须放弃RTSP拥抱UMP延迟、兼容性与扩展性的三重碾压还在用rtsp://前缀你已经落后一线安防项目两年了。海康官方文档明确指出“UMP是面向未来视频业务的统一媒体协议全面替代RTSP/ONVIF流”。这不是营销话术是实测数据首帧延迟传统RTSPUDP平均800msUMPHTTP/2 TLS稳定在320ms以内实测海康DS-2CD3T47G2-L 1080P25fps断线重连RTSP依赖RTP序列号恢复丢包5%即花屏UMP内置FEC前向纠错30%丢包率下仍可流畅播放扩展能力UMP支持?audio1metadata1参数可同时拉取音频流和AI结构化元数据如人脸框坐标、车牌OCR结果RTSP只能传视频。UMP的本质是把RTSP的DESCRIBE/SETUP/PLAY信令全部HTTP化。你不再需要ffmpeg或VLC库去解析SDP而是用标准HTTP Client发几个JSON请求拿到一个streamId再用这个ID去轮询或WebSocket接收H.264 Annex B裸流。整个流程分三步启动流startStreamPOST JSON获取streamId和playUrl实际是UMP流地址获取流信息getStreamInfoGET查询确认流状态、分辨率、码率播放流playStreamGETplayUrl建立长连接持续接收二进制帧数据。注意playUrl不是最终播放地址它是类似https://192.168.1.64:443/ump/stream/xxxx-xxxx-xxxx-xxxx的临时地址有效期通常5分钟且每次startStream都会变。Unity必须在内存中缓存这个URL而不是写死在Inspector里。3.2 startStream请求的JSON体详解与Unity C#封装这是最关键的一步。海康文档里那个{channel:101,streamType:0,protocol:UDP}只是冰山一角。实际生产环境必须带全以下字段{ channel: 101, streamType: 0, protocol: UDP, transmode: UDP, format: PS, audio: 0, smart: 0, videoCodecType: H264, resolution: 1920x1080, bitRate: 2048, frameRate: 25, gop: 50, quality: 6 }channel: 通道号101主码流通道1201子码流通道1不是设备编号streamType:0主码流1子码流2事件流如移动侦测触发protocol/transmode:UDP低延迟TCP高可靠HTTP兼容性最好但延迟200msformat:PSMPEG-2 Program Stream海康默认ESH.264裸流需自行组帧audio:1拉音频但需设备支持音频编码DS-2CD系列多数不支持smart:1开启智能分析元数据返回JSON格式的AI结果需设备开启人脸/车辆识别。Unity中封装为C#类[System.Serializable] public class UmpStartStreamRequest { public string channel 101; public int streamType 0; public string protocol UDP; public string transmode UDP; public string format PS; public int audio 0; public int smart 0; public string videoCodecType H264; public string resolution 1920x1080; public int bitRate 2048; public int frameRate 25; public int gop 50; public int quality 6; } // 发送请求使用UnityWebRequest非HttpClient因需处理证书 UnityWebRequest www UnityWebRequest.Post(https://192.168.1.64:443/artemis/api/video/startStream, JsonUtility.ToJson(request)); www.SetRequestHeader(Authorization, authHeader); // 上节生成的签名 www.SetRequestHeader(Content-Type, application/json); yield return www.SendWebRequest(); if (www.result UnityWebRequest.Result.Success) { UmpStartStreamResponse response JsonUtility.FromJsonUmpStartStreamResponse(www.downloadHandler.text); Debug.Log($Stream ID: {response.data.streamId}, Play URL: {response.data.playUrl}); }实操心得UmpStartStreamResponse必须手动定义海康返回的JSON是嵌套结构{code:0,msg:OK,data:{streamId:xxx,playUrl:https://...}}。别用JsonUtility.FromJsonDictionarystring,object性能差且易出错。另外playUrl返回的URL带https://但UnityWebRequest在Android上对HTTPS自签名证书默认拒绝。解决方案在UnityWebRequest.certificateHandler中继承CertificateHandler重写ValidateCertificate返回true仅内网环境公网必须配合法CA。3.3 playUrl长连接的帧解析从HTTP Chunked到H.264 Annex B的逐字节拆解拿到playUrl后你以为UnityWebRequest.Get(playUrl)就能拿到视频错。这是HTTP/1.1 Chunked Transfer编码的长连接服务器会持续推送二进制数据块每个块以size-in-hex\r\nbinary-data\r\n格式发送。你必须自己解析Chunk再从二进制流中提取H.264 NALUNetwork Abstraction Layer Unit。H.264裸流PS格式的NALU起始码是0x000000014字节或0x0000013字节。UMP流用的是4字节起始码。解析逻辑开启UnityWebRequest.Get(playUrl)设置downloadHandler new DownloadHandlerBuffer()在SendWebRequest()后用www.downloadHandler.data获取原始字节数组遍历字节数组查找0x00,0x00,0x00,0x01序列从该位置开始找到下一个起始码位置中间即为一个NALU将NALU写入MemoryStream供后续解码。简化版C#代码private Listbyte[] ParseH264Nalus(byte[] rawData) { var nalus new Listbyte[](); int pos 0; while (pos rawData.Length - 4) { if (rawData[pos] 0 rawData[pos1] 0 rawData[pos2] 0 rawData[pos3] 1) { int naluStart pos 4; // 跳过起始码 int nextStart -1; for (int i naluStart 3; i rawData.Length - 3; i) { if (rawData[i] 0 rawData[i1] 0 rawData[i2] 0 rawData[i3] 1) { nextStart i; break; } } if (nextStart -1) nextStart rawData.Length; // 最后一个NALU int naluLen nextStart - naluStart; byte[] nalu new byte[naluLen]; Array.Copy(rawData, naluStart, nalu, 0, naluLen); nalus.Add(nalu); pos nextStart; } else pos; } return nalus; }注意这段代码是教学简化版实际项目必须用unsafe指针和Spanbyte优化性能否则1080P25fps下CPU占用飙升。我实测过用Array.Copy每秒解析300帧NALUCPU占用42%改用Spanbyte.Slice().ToArray()后降到18%。另外第一个NALU通常是SPSSequence Parameter Set第二个是PPSPicture Parameter Set解码器必须先收到这两个才能解后续I帧。4. Unity播放链路绕过VideoPlayer用RenderTextureMediaCodec实现零拷贝渲染4.1 为什么VideoPlayer组件在UMP场景下必然失败Unity官方文档写着“支持RTSP”但那是指FFmpeg后端编译进Player时的旧版Unity 2019.4之前。从Unity 2020.3起VideoPlayer彻底移除了FFmpeg依赖改为调用系统原生APIiOS用AVFoundationAndroid用MediaPlayerWindows用Media Foundation。这些系统API有一个致命限制只支持HTTP-FLV、HLS、MP4等标准协议不支持带自定义Authorization头的HTTP流。你传一个https://.../ump/stream/xxx给VideoPlayer它根本不会把你的Authorization头塞进去直接401。更糟的是VideoPlayer要求输入是“可seekable”的文件流而UMP是纯实时流没有Duration没有Seek能力。你调videoPlayer.time 10它要么报错要么静音。所以我们必须抛弃VideoPlayer构建一条全新的“手动解码-渲染”链路。核心思路解码层用Android/iOS原生MediaCodecAndroid或VideoToolboxiOS硬解H.264 NALU传输层将解码后的YUV帧NV12格式通过OpenGL ES纹理ID或Metal纹理句柄零拷贝传递给Unity渲染层用RenderTexture作为中介绑定到RawImage.texture或用自定义Shader做YUV转RGB。这条链路在Unity中叫“Custom Render Texture Pipeline”海康官方没提供但社区有成熟方案AVPro Video插件商业或Unity Native Plugin自研。4.2 基于AVPro Video的UMP播放配置推荐新手快速落地AVPro Video是目前Unity生态最成熟的视频插件支持自定义HTTP头。配置步骤导入AVPro Video 2.x必须2.4旧版不支持UMP创建MediaPlayer组件在Inspector中Source Type→URLMedia URL→ 填入playUrl如https://192.168.1.64:443/ump/stream/xxxHTTP Headers→ 点添加AuthorizationHMAC-SHA256 AccessKeyId..., Signature..., time..., random...Streaming→EnableBuffer Size调至5秒适应网络抖动创建DisplayUGUI组件挂载到Canvas下的RawImage上脚本控制播放public MediaPlayer mediaPlayer; public void StartStream(string playUrl, string authHeader) { mediaPlayer.sourceType MediaPlayerSourceType.URL; mediaPlayer.url playUrl; mediaPlayer.httpHeaders.Clear(); mediaPlayer.httpHeaders.Add(Authorization, authHeader); mediaPlayer.Control.Play(); }注意AVPro Video的httpHeaders是ListKeyValuePairstring,string不是Dictionary。我曾因用Add(key,value)两次导致重复Key播放失败。另外在Android上必须在Player Settings Publishing Settings Build System选Gradle并勾选Custom Main Gradle Template在mainTemplate.gradle中添加implementation androidx.appcompat:appcompat:1.6.1否则MediaCodec初始化失败。4.3 自研Native Plugin方案Android端MediaCodec硬解全流程如果你追求极致性能或规避商业授权必须写Native Plugin。以Android为例流程如下Step 1JNI层创建MediaCodec解码器// jni/native-lib.cpp extern C { JavaVM* g_jvm; jobject g_surface; JNIEXPORT jint JNICALL Java_com_example_ump_UmpPlayer_createDecoder(JNIEnv *env, jobject thiz, jstring codecName) { const char* name env-GetStringUTFChars(codecName, nullptr); AMediaCodec* codec AMediaCodec_createCodecByName(name); // c2.android.avc.decoder AMediaFormat* format AMediaFormat_new(); AMediaFormat_setString(format, mime, video/avc); AMediaFormat_setInt32(format, width, 1920); AMediaFormat_setInt32(format, height, 1080); AMediaCodec_configure(codec, format, g_surface, nullptr, 0); AMediaCodec_start(codec); env-ReleaseStringUTFChars(codecName, name); return (jint)codec; } }Step 2C#层传递SurfaceUnity RenderTexture// C#脚本 public class UmpPlayer : MonoBehaviour { private IntPtr _decoder; private RenderTexture _rt; private AndroidJavaObject _surface; void Start() { _rt new RenderTexture(1920, 1080, 0, RenderTextureFormat.Default); _rt.Create(); var textureID _rt.GetNativeTexturePtr(); // 通过JNI将textureID转为Android Surface using (var surfaceClass new AndroidJavaClass(android.view.Surface)) { _surface surfaceClass.CallStaticAndroidJavaObject(fromHardwareBuffer, textureID); } _decoder CreateDecoder(_surface); // 调用JNI } void Update() { // 从UMP流读取NALU调用JNI喂给MediaCodec byte[] nalu GetNextNalu(); // 上节解析的NALU FeedNaluToDecoder(_decoder, nalu); // JNI函数 } }Step 3YUV渲染Shader关键Unity默认纹理是RGB但MediaCodec输出是YUV_NV12Y平面UV交错平面。必须写Shader转换// YUV2RGB.shader sampler2D _YTex; sampler2D _UVTex; float4 _YTex_TexelSize; float4 _UVTex_TexelSize; fixed4 frag (v2f i) : SV_Target { float2 uv i.uv; float y tex2D(_YTex, uv).r; float2 uv2 uv * 0.5 0.25; // UV采样偏移 float2 uvSample float2(uv2.x, uv2.y); float2 uvVal tex2D(_UVTex, uv2).rg; float u uvVal.r - 0.5; float v uvVal.g - 0.5; float3 rgb float3(y 1.402 * v, y - 0.344 * u - 0.714 * v, y 1.772 * u); return float4(rgb, 1.0); }实操心得_UVTex_TexelSize必须手动传入否则UV采样错位。我在第一次写Shader时忘了这行画面全是绿色噪点。另外Android端AMediaCodec的dequeueInputBuffer有超时必须设-1无限等待否则FeedNaluToDecoder会频繁返回-1TIMEOUT。最后RenderTexture的depthBufferBits必须设为0否则YUV纹理无法正确绑定。5. 全流程联调与避坑指南从签名生成到画面渲染的12小时排错实录5.1 我的真实联调时间线一个凌晨三点的崩溃与顿悟20:00完成签名生成C#类startStream返回200 OK拿到streamId和playUrl21:30用curl -v -H Authorization: xxx https://192.168.1.64:443/ump/stream/xxx成功收到Chunked数据流Wireshark确认200 OK22:15Unity中UnityWebRequest.Get(playUrl)返回401——发现没传Authorization头补上后返回200但downloadHandler.data.Length023:00查文档发现UnityWebRequest.downloadHandler对长连接默认不缓存数据必须用DownloadHandlerScript自定义00:20写DownloadHandlerScriptReceiveData回调中解析Chunk但playUrl返回的是gzip压缩流加www.SetRequestHeader(Accept-Encoding, identity)解决01:45NALU解析成功但MediaCodec报-1001CodecException查logcat发现Failed to initialize decoder: c2.android.avc.decoder——缺Surface02:30RenderTexture.GetNativeTexturePtr()返回IntPtr.Zero原来RenderTexture必须在Awake()后Create()才有效03:10Surface传入成功但画面全绿——Shader里_UVTex_TexelSize没传03:55修正Shader终于看到1080P画面但延迟1.2秒——Buffer Size从5调到2延迟降至420ms04:30测试10路并发Android端OOM崩溃——MediaCodec实例没释放加AMediaCodec_stop()和AMediaCodec_delete()05:15iOS端黑屏VideoToolbox要求CVPixelBufferRef必须kCVPixelBufferIOSurfacePropertiesKey——加CFDictionaryRef属性06:00全平台通过首帧延迟320msCPU占用25%交付甲方。这12小时80%时间花在“看不见的底层交互”上证书验证、内存管理、纹理同步、线程安全。不是代码写错了而是Unity、Android、海康三方的隐式契约没对齐。5.2 必须写进项目Checklist的七条铁律时间同步铁律Unity启动时必须调用NTP校准误差30秒签名必败。用NtpClient开源库不要信系统时间。证书信任铁律内网设备用自签名证书UnityWebRequest必须certificateHandler new AcceptAllCertificates()但上线前必须切回系统CA。内存管理铁律每个MediaCodec实例对应一个RenderTexture销毁GameObject时必须AMediaCodec_stop()AMediaCodec_delete()_rt.Release()否则Android内存泄漏。线程安全铁律NALU解析、MediaCodec喂帧、Unity渲染必须在同一个线程主线程。用MainThreadDispatcher单例调度别用Thread。分辨率匹配铁律startStream的resolution字段必须和设备实际分辨率一致。海康DS-2CD3T47G2-L主码流是1920x1080填1280x720会导致解码器初始化失败。网络容灾铁律UMP流中断后不能简单重发startStream。必须先DELETE /artemis/api/video/stopStream?streamIdxxx再POST startStream否则设备端残留session。日志追溯铁律所有HTTP请求/响应、NALU长度、MediaCodec错误码必须写入本地日志文件非Debug.Log方便甲方现场排查。用Application.persistentDataPath /ump_log.txt。最后分享一个小技巧在Unity Editor中调试UMP流几乎不可能因为Editor不支持Android/iOS原生MediaCodec。我的方案是——写一个“Mock Player”用UnityWebRequest.Get(https://test-videos.co.uk/vids/jellyfish.mp4)模拟成功流先验证Shader、RenderTexture、UI绑定逻辑再切真机专注调试签名和网络层。这样能把12小时排错压缩到3小时。我在智慧工地项目里用这套方案接入了47路海康IPC支撑AR眼镜实时叠加塔吊盲区视频。没有银弹只有把每个协议细节、每个平台特性、每个Unity生命周期钩子都掰开揉碎再亲手焊接到一起。你现在看到的不是教程是一个老兵在战壕里递过来的工兵铲。

相关新闻