Procházet zdrojové kódy

feat(video-center): 接入海康 H5player 多分屏实时预览

- 新增监控点懒加载树及在线状态展示
- 接入 H5player 2.5.1 SDK 和预览地址接口
- 支持 1、4、9、16 分屏切换、全屏及侧栏折叠
- 完善窗口级 loading、首帧、断流和错误状态处理
- 优化多窗口并发预览、尺寸响应和播放器销毁流程
- 新增分屏图标资源并更新 Iconify 依赖
Zimo před 1 týdnem
rodič
revize
001785d155

+ 1 - 1
package.json

@@ -119,7 +119,7 @@
   "devDependencies": {
     "@commitlint/cli": "^19.0.1",
     "@commitlint/config-conventional": "^19.0.0",
-    "@iconify/json": "^2.2.187",
+    "@iconify/json": "^2.2.524",
     "@intlify/unplugin-vue-i18n": "^2.0.0",
     "@purge-icons/generated": "^0.9.0",
     "@types/lodash-es": "^4.17.12",

+ 6 - 6
pnpm-lock.yaml

@@ -286,8 +286,8 @@ importers:
         specifier: ^19.0.0
         version: 19.6.0
       '@iconify/json':
-        specifier: ^2.2.187
-        version: 2.2.277
+        specifier: ^2.2.524
+        version: 2.2.524
       '@intlify/unplugin-vue-i18n':
         specifier: ^2.0.0
         version: 2.0.0(rollup@4.27.4)(vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)))
@@ -1421,8 +1421,8 @@ packages:
     resolution: {integrity: sha512-1nemfyD/OJzh9ALepH7YfuuP8BdEB24Skhd8DXWh0hzcOxImbb1ZizSZkpCzAwSZSGcJFmscIBaBQu+yLyWaxQ==}
     deprecated: no longer maintained, switch to modern iconify-icon web component
 
-  '@iconify/json@2.2.277':
-    resolution: {integrity: sha512-hNBnGD2djNgsdB4Yq5dBhP2CI0PLt+4EamozKSAD+hsbFAzVUN6sMj5FUiBFu8BKUOBIYcrX8ri7C7Qe3K10ew==}
+  '@iconify/json@2.2.524':
+    resolution: {integrity: sha512-xw2TRuiRcOzx8mTXRNIfg9FYr4sd7Z7s/DGKitJZt3njeiFmjHte8qKWEGK+IePYtQ1QyVnhfOi+5sEjVTmvDQ==}
 
   '@iconify/types@2.0.0':
     resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -7857,10 +7857,10 @@ snapshots:
     dependencies:
       '@iconify/types': 2.0.0
 
-  '@iconify/json@2.2.277':
+  '@iconify/json@2.2.524':
     dependencies:
       '@iconify/types': 2.0.0
-      pathe: 1.1.2
+      pathe: 2.0.3
 
   '@iconify/types@2.0.0': {}
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/h5player.min.js


+ 705 - 0
public/h5player/playctrl1/DecodeWorker.js

@@ -0,0 +1,705 @@
+/**
+ * Created by wangweijie5 on 2016/12/5.
+ */
+(function (event) {
+    const AUDIO_TYPE = 0;	// 音频
+    const VIDEO_TYPE = 1;   // 视频
+    const PRIVT_TYPE = 2;  // 私有帧
+
+    const PLAYM4_AUDIO_FRAME = 100; // 音频帧
+    const PLAYM4_VIDEO_FRAME = 101; // 视频帧
+
+    const PLAYM4_OK = 1;
+    const PLAYM4_ORDER_ERROR = 2;
+    const PLAYM4_DECODE_ERROR = 44 	// 解码失败
+    const PLAYM4_NOT_KEYFRAME = 48; 	// 非关键帧
+    const PLAYM4_NEED_MORE_DATA = 31;   // 需要更多数据才能解析
+    const PLAYM4_NEED_NEET_LOOP = 35; //丢帧需要下个循环
+    const PLAYM4_SYS_NOT_SUPPORT = 16; 	// 不支持
+
+    // importScripts('Decoder.js');
+    // Module.addOnPostRun(function () {
+    //     postMessage({ 'function': "loaded" });
+    // });
+
+    var iStreamMode = 0;  // 流模式
+
+    var bOpenMode = false;
+    var bOpenStream = false;
+
+    var funGetFrameData = null;
+    var funGetAudFrameData = null;
+
+    var bWorkerPrintLog = false;//worker层log开关
+
+    var g_nPort = -1;
+    var pInputData = null;
+    var inputBufferSize = 40960;
+
+    self.JSPlayM4_RunTimeInfoCallBack = function (nPort, pstRunTimeInfo, pUser) {
+        let port = nPort;
+        let user = pUser;
+        let nRunTimeModule = Module.HEAP32[pstRunTimeInfo >> 2];
+        let nStrVersion = Module.HEAP32[pstRunTimeInfo + 4 >> 2];
+        let nFrameTimeStamp = Module.HEAP32[pstRunTimeInfo + 8 >> 2];
+        let nFrameNum = Module.HEAP32[pstRunTimeInfo + 12 >> 2];
+        let nErrorCode = Module.HEAP32[pstRunTimeInfo + 16 >> 2];
+        // console.log("nRunTimeModule:"+nRunTimeModule+",nFrameNum:"+nFrameNum+",nErrorCode:"+nErrorCode);
+        postMessage({ 'function': "RunTimeInfoCallBack", 'nRunTimeModule': nRunTimeModule, 'nStrVersion': nStrVersion, 'nFrameTimeStamp': nFrameTimeStamp, 'nFrameNum': nFrameNum, 'nErrorCode': nErrorCode });
+    }
+	
+	self.JSPlayM4_StreamInfoCallBack = function (nPort, pstStreamInfo, pUser)
+	{
+		let port = nPort;
+        let user = pUser;
+        let nSystemformat = Module.HEAP16[pstStreamInfo >> 1]; //封装类型
+        let nVideoformat = Module.HEAP16[pstStreamInfo + 2 >> 1];//视频编码类型
+        let nAudioformat = Module.HEAP16[pstStreamInfo + 4 >> 1];//音频编码类型
+        let nAudiochannels = Module.HEAP16[pstStreamInfo + 6 >> 1]; //音频通道数
+        let nAudiobitspersample = Module.HEAP32[pstStreamInfo + 8 >> 2];//音频样位率
+        let nAudiosamplesrate = Module.HEAP32[pstStreamInfo + 12 >> 2];//音频采样率
+        let nAudiobitrate = Module.HEAP32[pstStreamInfo + 16 >> 2];//音频比特率,单位:bit
+		//console.log("nSystemformat:" + nSystemformat + ",nVideoformat:" + nVideoformat + ",nAudioformat:" + nAudioformat + ",nAudiochannels:" + nAudiochannels + ",nAudiobitspersample:" + nAudiobitspersample + ",nAudiosamplesrate:" + nAudiosamplesrate + ",nAudiobitrate:" + nAudiobitrate);
+        postMessage({ 'function': "StreamInfoCallBack", 'nSystemformat': nSystemformat, 'nVideoformat': nVideoformat, 'nAudioformat': nAudioformat, 'nAudiochannels': nAudiochannels, 'nAudiobitspersample': nAudiobitspersample, 'nAudiosamplesrate': nAudiosamplesrate, 'nAudiobitrate': nAudiobitrate});
+	}
+
+    onmessage = function (event) {
+        var eventData = event.data;
+        var res = 0;
+        switch (eventData.command) {
+            case "importScripts":
+                const decodebase = eventData.data + "Decoder.js"
+                importScripts(decodebase);
+                Module.addOnPostRun(function () {
+                    postMessage({ 'function': "loaded" });
+                });
+                break;
+            case "printLog":
+                let downloadFlag = eventData.data;
+                if (downloadFlag === true) {
+                    bWorkerPrintLog = true;
+                    res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
+                }
+                else {
+                    bWorkerPrintLog = false;
+                    res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
+                }
+
+                if (res !== PLAYM4_OK) {
+                    console.log("DecodeWorker.js: PlayerSDK print log failed,res" + res);
+                    postMessage({ 'function': "printLog", 'errorCode': res });
+                }
+                break;
+            case "SetPlayPosition":
+                let nFrameNumOrTime = eventData.data;
+                let enPosType = eventData.type;
+                // res = Module._SetPlayPosition(nFrameNumOrTime,enPosType);
+                // if (res !== PLAYM4_OK)
+                // {
+                //     postMessage({'function': "SetPlayPosition", 'errorCode': res});
+                //     return;
+                // }
+                // //有没有buffer需要清除
+
+                break;
+            case "SetStreamOpenMode":
+                //获取端口号
+                g_nPort = Module._GetPort();
+                //设置流打开模式
+                iStreamMode = eventData.data;
+                res = Module._SetStreamOpenMode(g_nPort, iStreamMode);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetStreamOpenMode", 'errorCode': res });
+                    return;
+                }
+                bOpenMode = true;
+                break;
+
+            case "OpenStream":
+                // 接收到的数据
+                var iHeadLen = eventData.dataSize;
+                var pHead = Module._malloc(iHeadLen + 4);
+                if (pHead === null) {
+                    return;
+                }
+                var aHead = Module.HEAPU8.subarray(pHead, pHead + iHeadLen);
+                aHead.set(new Uint8Array(eventData.data));
+                res = Module._OpenStream(g_nPort, pHead, iHeadLen, eventData.bufPoolSize);
+                postMessage({ 'function': "OpenStream", 'errorCode': res });
+                if (res !== PLAYM4_OK) {
+                    //释放内存
+                    Module._free(pHead);
+                    pHead = null;
+                    return;
+                }
+                bOpenStream = true;
+                break;
+            case "Play":
+                let resP = Module._Play(g_nPort);
+                if (resP !== PLAYM4_OK) {
+                    return;
+                }
+                break;
+            case "InputData":
+                // 接收到的数据
+                var iLen = eventData.dataSize;
+                if (iLen > 0) {
+                    if (pInputData == null || iLen > inputBufferSize) {
+                        if (pInputData != null) {
+                            Module._free(pInputData);
+                            pInputData = null;
+                        }
+                        if (iLen > inputBufferSize) {
+                            inputBufferSize = iLen;
+                        }
+
+                        pInputData = Module._malloc(inputBufferSize);
+                        if (pInputData === null) {
+                            return;
+                        }
+                    }
+
+                    var inputData = new Uint8Array(eventData.data);
+                    // var aInputData = Module.HEAPU8.subarray(pInputData, pInputData + iLen);
+                    // aInputData.set(inputData);
+                    Module.writeArrayToMemory(inputData, pInputData);
+                    inputData = null;
+                    res = Module._InputData(g_nPort, pInputData, iLen);
+                    if (res !== PLAYM4_OK) {
+                        let errorCode = Module._GetLastError(g_nPort);
+                        let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
+                        postMessage({ 'function': "InputData", 'errorCode': errorCode, "sourceRemain": sourceRemain });
+                    }
+                    //Module._free(pInputData);
+                    //pInputData = null;
+                } else {
+                    let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
+                    if (sourceRemain == 0) {
+                        postMessage({ 'function': "InputData", 'errorCode': PLAYM4_NEED_MORE_DATA });
+                        return;
+                    }
+                }
+
+                /////////////////////
+                // if (funGetFrameData === null) {
+                //     funGetFrameData = Module.cwrap('GetFrameData', 'number');
+                // }
+
+                while (bOpenMode && bOpenStream) {
+
+                    var ret = getFrameData();
+                    // 直到获取视频帧或数据不足为止
+                    if (PLAYM4_VIDEO_FRAME === ret || PLAYM4_NEED_MORE_DATA === ret || PLAYM4_ORDER_ERROR === ret)//PLAYM4_VIDEO_FRAME === ret ||  || PLAYM4_NEED_NEET_LOOP === ret
+                    {
+                        break;
+                    }
+                }
+                break;
+
+            case "SetSecretKey":
+                var keyLen = eventData.nKeyLen;
+                var pKeyData = Module._malloc(keyLen);
+                if (pKeyData === null) {
+                    return;
+                }
+                var nKeySize = eventData.data.length
+                var bufData = stringToBytes(eventData.data);
+                var aKeyData = Module.HEAPU8.subarray(pKeyData, pKeyData + keyLen);
+                let u8array = new Uint8Array(keyLen);
+                aKeyData.set(u8array, 0);
+                aKeyData.set(new Uint8Array(bufData));
+                aKeyData = null;
+                u8array = null;
+
+                res = Module._SetSecretKey(g_nPort, eventData.nKeyType, pKeyData, keyLen);//, nKeySize
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetSecretKey", 'errorCode': res });
+                    Module._free(pKeyData);
+                    pKeyData = null;
+                    return;
+                }
+
+                Module._free(pKeyData);
+                pKeyData = null;
+                break;
+
+            case "GetBMP":
+                var nBMPWidth = eventData.width;
+                var nBMPHeight = eventData.height;
+                var pYUVData = eventData.data;
+                var nYUVSize = nBMPWidth * nBMPHeight * 3 / 2;
+                var oBMPCropRect = {
+                    left: eventData.left,
+                    top: eventData.top,
+                    right: eventData.right,
+                    bottom: eventData.bottom
+                };
+
+                var pDataYUV = Module._malloc(nYUVSize);
+                if (pDataYUV === null) {
+                    return;
+                }
+
+                Module.writeArrayToMemory(new Uint8Array(pYUVData, 0, nYUVSize), pDataYUV);
+
+                // 分配BMP空间
+                var nBmpSize = nBMPWidth * nBMPHeight * 4 + 60;
+                var pBmpData = Module._malloc(nBmpSize);
+                var pBmpSize = Module._malloc(4);
+                if (pBmpData === null || pBmpSize === null) {
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+
+                    if (pBmpData != null) {
+                        Module._free(pBmpData);
+                        pBmpData = null;
+                    }
+
+                    if (pBmpSize != null) {
+                        Module._free(pBmpSize);
+                        pBmpSize = null;
+                    }
+                    return;
+                }
+
+                //Module._memset(pBmpSize, nBmpSize, 4); // 防止bmp截图出现输入数据过大的错误码
+                Module.setValue(pBmpSize, nBmpSize, "i32");
+                res = Module._GetBMP(g_nPort, pDataYUV, nYUVSize, pBmpData, pBmpSize,
+                    oBMPCropRect.left, oBMPCropRect.top, oBMPCropRect.right, oBMPCropRect.bottom);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "GetBMP", 'errorCode': res });
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+                    Module._free(pBmpData);
+                    pBmpData = null;
+                    Module._free(pBmpSize);
+                    pBmpSize = null;
+                    return;
+                }
+
+                // 获取BMP图片大小
+                var nBmpDataSize = Module.getValue(pBmpSize, "i32");
+
+                // 获取BMP图片数据
+                var aBmpData = new Uint8Array(nBmpDataSize);
+                aBmpData.set(Module.HEAPU8.subarray(pBmpData, pBmpData + nBmpDataSize));
+
+                postMessage({ 'function': "GetBMP", 'data': aBmpData, 'errorCode': res }, [aBmpData.buffer]);
+                aBmpData = null;
+                if (pDataYUV != null) {
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+                }
+                if (pBmpData != null) {
+                    Module._free(pBmpData);
+                    pBmpData = null;
+                }
+                if (pBmpSize != null) {
+                    Module._free(pBmpSize);
+                    pBmpSize = null;
+                }
+                break;
+
+            case "GetJPEG":
+                var nJpegWidth = eventData.width;
+                var nJpegHeight = eventData.height;
+                var pYUVData1 = eventData.data;
+                var nYUVSize1 = nJpegWidth * nJpegHeight * 3 / 2;
+                var oJpegCropRect = {
+                    left: eventData.left,
+                    top: eventData.top,
+                    right: eventData.right,
+                    bottom: eventData.bottom
+                };
+
+                var pDataYUV1 = Module._malloc(nYUVSize1);
+                if (pDataYUV1 === null) {
+                    return;
+                }
+
+                Module.writeArrayToMemory(new Uint8Array(pYUVData1, 0, nYUVSize1), pDataYUV1);
+
+                // 分配JPEG空间
+                var pJpegData = Module._malloc(nYUVSize1);
+                var pJpegSize = Module._malloc(4);
+                if (pJpegData === null || pJpegSize === null) {
+                    if (pJpegData != null) {
+                        Module._free(pJpegData);
+                        pJpegData = null;
+                    }
+
+                    if (pJpegSize != null) {
+                        Module._free(pJpegSize);
+                        pJpegSize = null;
+                    }
+
+                    if (pDataYUV1 != null) {
+                        Module._free(pDataYUV1);
+                        pDataYUV1 = null;
+                    }
+                    return;
+                }
+
+                Module.setValue(pJpegSize, nJpegWidth * nJpegHeight * 2, "i32");    // JPEG抓图,输入缓冲长度不小于当前帧YUV大小
+
+                res = Module._GetJPEG(g_nPort, pDataYUV1, nYUVSize1, pJpegData, pJpegSize,
+                    oJpegCropRect.left, oJpegCropRect.top, oJpegCropRect.right, oJpegCropRect.bottom);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "GetJPEG", 'errorCode': res });
+                    if (pJpegData != null) {
+                        Module._free(pJpegData);
+                        pJpegData = null;
+                    }
+
+                    if (pJpegSize != null) {
+                        Module._free(pJpegSize);
+                        pJpegSize = null;
+                    }
+
+                    if (pDataYUV1 != null) {
+                        Module._free(pDataYUV1);
+                        pDataYUV1 = null;
+                    }
+                    return;
+                }
+
+                // 获取JPEG图片大小
+                var nJpegSize = Module.getValue(pJpegSize, "i32");
+
+                // 获取JPEG图片数据
+                var aJpegData = new Uint8Array(nJpegSize);
+                aJpegData.set(Module.HEAPU8.subarray(pJpegData, pJpegData + nJpegSize));
+
+                postMessage({ 'function': "GetJPEG", 'data': aJpegData, 'errorCode': res }, [aJpegData.buffer]);
+
+                nJpegSize = null;
+                aJpegData = null;
+
+                if (pDataYUV1 != null) {
+                    Module._free(pDataYUV1);
+                    pDataYUV1 = null;
+                }
+                if (pJpegData != null) {
+                    Module._free(pJpegData);
+                    pJpegData = null;
+                }
+                if (pJpegSize != null) {
+                    Module._free(pJpegSize);
+                    pJpegSize = null;
+                }
+                break;
+
+            case "SetDecodeFrameType":
+                var nFrameType = eventData.data;
+                res = Module._SetDecodeFrameType(g_nPort, nFrameType);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetDecodeFrameType", 'errorCode': res });
+                    return;
+                }
+                break;
+            case "CloseStream":
+                //stop
+                let resS = Module._Stop(g_nPort);
+                if (resS !== PLAYM4_OK) {
+                    postMessage({ 'function': "Stop", 'errorCode': res });
+                    return;
+                }
+                //closeStream
+                res = Module._CloseStream(g_nPort);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "CloseStream", 'errorCode': res });
+                    return;
+                }
+                //freePort
+                let resF = Module._FreePort(g_nPort);
+                if (resF !== PLAYM4_OK) {
+                    postMessage({ 'function': "FreePort", 'errorCode': res });
+                    return;
+                }
+                if (pInputData != null) {
+                    Module._free(pInputData);
+                    pInputData = null;
+                }
+                break;
+            case "PlaySound":
+                let resPS = Module._PlaySound(g_nPort);
+                if (resPS !== PLAYM4_OK) {
+                    console.log("PlaySound failed");
+                    return;
+                }
+                break;
+            case "StopSound":
+                let resSS = Module._StopSound();
+                if (resSS !== PLAYM4_OK) {
+                    console.log("StopSound failed");
+                    return;
+                }
+                break;
+            case "SetVolume":
+                let resSV = Module._SetVolume(g_nPort, eventData.volume);
+                if (resSV !== PLAYM4_OK) {
+                    console.log("Audio SetVolume failed");
+                    return;
+                }
+                break;
+            case "GetVolume":
+                let volume = Module._GetVolume();
+                if (volume > 0) {
+                    postMessage({ 'function': "GetVolume", 'volume': volume });
+                }
+                else {
+                    console.log("Audio GetVolume failed");
+                    return;
+                }
+                break;
+            case "OnlyPlaySound":
+                let resOPS = Module._OnlyPlaySound(g_nPort);
+                if (resOPS !== PLAYM4_OK) {
+                    console.log("OnlyPlaySound failed");
+                    return;
+                }
+                break;
+            case "Pause":
+                let resPa = Module._Pause(g_nPort, eventData.bPlay);
+                if (resPa !== PLAYM4_OK) {
+                    console.log("Pause failed");
+                    return;
+                }
+            case "PlayRate":
+                Module._SetPlayRate(g_nPort, eventData.playRate);
+                break;
+            case "SetIFrameDecInterval":
+                Module._SetIFrameDecInterval(g_nPort, eventData.data);
+                break;
+            case "SetLostFrameMode":
+                Module._SetLostFrameMode(g_nPort, eventData.data, 0);
+                break;
+            case "SetDemuxModel":
+                let resSDM = Module._SetDemuxModel(g_nPort, eventData.nIdemuxType, eventData.bTrue);
+                break;
+            case "SkipErrorData":
+                Module._SkipErrorData(g_nPort, eventData.bSkip);
+                break;
+            case "SetDecodeERC":
+                Module._SetDecodeERC(g_nPort, eventData.nLevel);
+                break;
+            case "SetANRParam":
+                Module._SetANRParam(g_nPort, eventData.nEnable, eventData.nANRLevel);
+                break;
+            case "SetResampleValue":
+                Module._SetResampleValue(g_nPort, eventData.nEnable, eventData.resampleValue);
+                break;
+            case "GetLastError":
+                let errorCode = Module._GetLastError(g_nPort);
+                postMessage({ 'function': "GetLastError", 'errorCode': errorCode });
+                break;
+            case "SetGlobalBaseTime":
+                Module._SetGlobalBaseTime(g_nPort, eventData.year, eventData.month, eventData.day, eventData.hour, eventData.min, eventData.sec, eventData.ms);
+                break;
+            case "SetRunTimeInfoCB":
+                Module._SetRunTimeInfoCallBackEx(g_nPort, eventData.nModuleType, 0);
+                break;
+			case "SetStreamInfoCB":
+                Module._SetStreamInfoCallBack(g_nPort, eventData.nType, 0);
+                break;
+			case "ResetBuffer":
+                Module._JSPlayM4_ResetBuffer(g_nPort, eventData.type);
+                break;
+            default:
+                break;
+        }
+    };
+
+    function getOSDTime(oFrameInfo) {
+        var iYear = oFrameInfo.year;
+        var iMonth = oFrameInfo.month;
+        var iDay = oFrameInfo.day;
+        var iHour = oFrameInfo.hour;
+        var iMinute = oFrameInfo.minute;
+        var iSecond = oFrameInfo.second;
+        var iMiSecond = oFrameInfo.misecond
+
+        if (iMonth < 10) {
+            iMonth = "0" + iMonth;
+        }
+        if (iDay < 10) {
+            iDay = "0" + iDay;
+        }
+        if (iHour < 10) {
+            iHour = "0" + iHour;
+        }
+        if (iMinute < 10) {
+            iMinute = "0" + iMinute;
+        }
+        if (iSecond < 10) {
+            iSecond = "0" + iSecond;
+        }
+        let osdTime = {};
+
+        osdTime.year = iYear;
+        osdTime.month = iMonth;
+        osdTime.week = 0;
+        osdTime.day = iDay;
+        osdTime.hour = iHour;
+        osdTime.minute = iMinute;
+        osdTime.second = iSecond;
+        osdTime.milliseconds = iMiSecond;
+        return osdTime;
+        //return iYear + "-" + iMonth + "-" + iDay + " " + iHour + ":" + iMinute + ":" + iSecond;
+    }
+    // 获取帧数据
+    function getFrameData() {
+        // function getFrameData() {
+        // 获取帧数据
+        var res = Module._GetFrameData();
+        //var res = fun();
+        if (res === PLAYM4_OK) {
+            var iFrameInfo = Module._GetFrameInfo();
+            let oFrameInfo = {};
+            oFrameInfo.frameType = Module.HEAP32[iFrameInfo >> 2];
+            oFrameInfo.frameSize = Module.HEAP32[iFrameInfo + 4 >> 2];
+            oFrameInfo.width = Module.HEAP32[iFrameInfo + 8 >> 2];
+            oFrameInfo.height = Module.HEAP32[iFrameInfo + 12 >> 2];
+            oFrameInfo.timeStamp = Module.HEAP32[iFrameInfo + 16 >> 2];
+            oFrameInfo.frameRate = Module.HEAP32[iFrameInfo + 20 >> 2];
+            oFrameInfo.bitsPerSample = Module.HEAP32[iFrameInfo + 24 >> 2];
+            oFrameInfo.samplesPerSec = Module.HEAP32[iFrameInfo + 28 >> 2];
+            oFrameInfo.channels = Module.HEAP32[iFrameInfo + 32 >> 2];
+            oFrameInfo.frameNum = Module.HEAP32[iFrameInfo + 36 >> 2];
+
+            oFrameInfo.cropLeft = Module.HEAP32[iFrameInfo + 40 >> 2];
+            oFrameInfo.cropRight = Module.HEAP32[iFrameInfo + 44 >> 2];
+            oFrameInfo.cropTop = Module.HEAP32[iFrameInfo + 48 >> 2];
+            oFrameInfo.cropBottom = Module.HEAP32[iFrameInfo + 52 >> 2];
+
+            oFrameInfo.year = Module.HEAP16[iFrameInfo + 64 >> 1];
+            oFrameInfo.month = Module.HEAP16[iFrameInfo + 66 >> 1];
+            oFrameInfo.day = Module.HEAP16[iFrameInfo + 68 >> 1];
+            oFrameInfo.hour = Module.HEAP16[iFrameInfo + 70 >> 1];
+            oFrameInfo.minute = Module.HEAP16[iFrameInfo + 72 >> 1];
+            oFrameInfo.second = Module.HEAP16[iFrameInfo + 74 >> 1];
+            oFrameInfo.misecond = Module.HEAP16[iFrameInfo + 76 >> 1];
+            switch (oFrameInfo.frameType) {
+                case AUDIO_TYPE:
+                    var iSize = oFrameInfo.frameSize;
+                    if (0 === iSize) {
+                        return -1;
+                    }
+                    var pPCM = Module._GetFrameBuffer();
+                    // var audioBuf = new ArrayBuffer(iSize);
+                    var aPCMData = new Uint8Array(iSize);
+                    aPCMData.set(Module.HEAPU8.subarray(pPCM, pPCM + iSize));
+                    if (bWorkerPrintLog) {
+                        console.log("<<<Worker: audio media Info: nSise:" + oFrameInfo.frameSize + ",nSampleRate:" + oFrameInfo.samplesPerSec + ',channel:' + oFrameInfo.channels + ',bitsPerSample:' + oFrameInfo.bitsPerSample);
+                    }
+                    postMessage({
+                        'function': "GetFrameData", 'type': "audioType", 'data': aPCMData.buffer,
+                        'frameInfo': oFrameInfo, 'errorCode': res
+                    }, [aPCMData.buffer]);
+
+                    oFrameInfo = null;
+                    pPCM = null;
+                    aPCMData = null;
+                    return PLAYM4_AUDIO_FRAME;
+
+                case VIDEO_TYPE:
+                    var szOSDTime = getOSDTime(oFrameInfo);
+
+                    var iWidth = oFrameInfo.width;
+                    var iHeight = oFrameInfo.height;
+
+                    var iYUVSize = iWidth * iHeight * 3 / 2;
+                    if (0 === iYUVSize) {
+                        return -1;
+                    }
+
+                    var pYUV = Module._GetFrameBuffer();
+
+                    // 图像数据渲染后压回,若从主码流切到子码流,存在数组大小与图像大小不匹配现象
+                    var aYUVData = new Uint8Array(iYUVSize);
+                    aYUVData.set(Module.HEAPU8.subarray(pYUV, pYUV + iYUVSize));
+                    if (bWorkerPrintLog) {
+                        console.log("<<<Worker: video media Info: Width:" + oFrameInfo.width + ",Height:" + oFrameInfo.height + ",timeStamp:" + oFrameInfo.timeStamp);
+                    }
+
+                    postMessage({
+                        'function': "GetFrameData", 'type': "videoType", 'data': aYUVData.buffer,
+                        'dataLen': aYUVData.length, 'osd': szOSDTime, 'frameInfo': oFrameInfo, 'errorCode': res
+                    }, [aYUVData.buffer]);
+
+                    oFrameInfo = null;
+                    pYUV = null;
+                    aYUVData = null;
+                    return PLAYM4_VIDEO_FRAME;
+
+                case PRIVT_TYPE:
+                    postMessage({
+                        'function': "GetFrameData", 'type': "", 'data': null,
+                        'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
+                    });
+                    return PLAYM4_SYS_NOT_SUPPORT;
+
+                default:
+                    postMessage({
+                        'function': "GetFrameData", 'type': "", 'data': null,
+                        'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
+                    });
+                    return PLAYM4_SYS_NOT_SUPPORT;
+            }
+        }
+        else {
+            let errorCode = Module._GetLastError(g_nPort);
+            //解码失败返回裸数据
+            if (PLAYM4_DECODE_ERROR === errorCode) {
+                var rawInfo = Module._GetRawDataInfo();
+                var pRawData = Module._GetRawDataBuffer();
+                var aRawData = new Uint8Array(rawInfo.isize);
+                aRawData.set(Module.HEAPU8.subarray(pRawData, pRawData + rawInfo.isize));
+                postMessage({
+                    'function': "GetRawData", 'type': "", 'data': aRawData.buffer,
+                    'rawDataLen': rawInfo.isize, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
+                });
+                rawInfo = null;
+                pRawData = null;
+                aRawData = null;
+            }
+            //需要更多数据
+            if (PLAYM4_NEED_MORE_DATA === errorCode || PLAYM4_SYS_NOT_SUPPORT === errorCode || PLAYM4_NEED_NEET_LOOP === errorCode) {
+                postMessage({
+                    'function': "GetFrameData", 'type': "", 'data': null,
+                    'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
+                });
+            }
+            return errorCode;
+        }
+    }
+
+    // 开始计算时间
+    function startTime() {
+        return new Date().getTime();
+    }
+
+    // 结束计算时间
+    function endTime() {
+        return new Date().getTime();
+    }
+
+    // 字母字符串转byte数组
+    function stringToBytes(str) {
+        var ch, st, re = [];
+        for (var i = 0; i < str.length; i++) {
+            ch = str.charCodeAt(i);  // get char
+            st = [];                 // set up "stack"
+            do {
+                st.push(ch & 0xFF);  // push byte to stack
+                ch = ch >> 8;          // shift value down by 1 byte
+            }
+            while (ch);
+            // add stack contents to result
+            // done because chars have "wrong" endianness
+            re = re.concat(st.reverse());
+        }
+        // return an array of bytes
+        return re;
+    }
+})();

binární
public/h5player/playctrl1/Decoder.js


+ 705 - 0
public/h5player/playctrl1simd/DecodeWorker.js

@@ -0,0 +1,705 @@
+/**
+ * Created by wangweijie5 on 2016/12/5.
+ */
+(function (event) {
+    const AUDIO_TYPE = 0;	// 音频
+    const VIDEO_TYPE = 1;   // 视频
+    const PRIVT_TYPE = 2;  // 私有帧
+
+    const PLAYM4_AUDIO_FRAME = 100; // 音频帧
+    const PLAYM4_VIDEO_FRAME = 101; // 视频帧
+
+    const PLAYM4_OK = 1;
+    const PLAYM4_ORDER_ERROR = 2;
+    const PLAYM4_DECODE_ERROR = 44 	// 解码失败
+    const PLAYM4_NOT_KEYFRAME = 48; 	// 非关键帧
+    const PLAYM4_NEED_MORE_DATA = 31;   // 需要更多数据才能解析
+    const PLAYM4_NEED_NEET_LOOP = 35; //丢帧需要下个循环
+    const PLAYM4_SYS_NOT_SUPPORT = 16; 	// 不支持
+
+    // importScripts('Decoder.js');
+    // Module.addOnPostRun(function () {
+    //     postMessage({ 'function': "loaded" });
+    // });
+
+    var iStreamMode = 0;  // 流模式
+
+    var bOpenMode = false;
+    var bOpenStream = false;
+
+    var funGetFrameData = null;
+    var funGetAudFrameData = null;
+
+    var bWorkerPrintLog = false;//worker层log开关
+
+    var g_nPort = -1;
+    var pInputData = null;
+    var inputBufferSize = 40960;
+
+    self.JSPlayM4_RunTimeInfoCallBack = function (nPort, pstRunTimeInfo, pUser) {
+        let port = nPort;
+        let user = pUser;
+        let nRunTimeModule = Module.HEAP32[pstRunTimeInfo >> 2];
+        let nStrVersion = Module.HEAP32[pstRunTimeInfo + 4 >> 2];
+        let nFrameTimeStamp = Module.HEAP32[pstRunTimeInfo + 8 >> 2];
+        let nFrameNum = Module.HEAP32[pstRunTimeInfo + 12 >> 2];
+        let nErrorCode = Module.HEAP32[pstRunTimeInfo + 16 >> 2];
+        // console.log("nRunTimeModule:"+nRunTimeModule+",nFrameNum:"+nFrameNum+",nErrorCode:"+nErrorCode);
+        postMessage({ 'function': "RunTimeInfoCallBack", 'nRunTimeModule': nRunTimeModule, 'nStrVersion': nStrVersion, 'nFrameTimeStamp': nFrameTimeStamp, 'nFrameNum': nFrameNum, 'nErrorCode': nErrorCode });
+    }
+	
+	self.JSPlayM4_StreamInfoCallBack = function (nPort, pstStreamInfo, pUser)
+	{
+		let port = nPort;
+        let user = pUser;
+        let nSystemformat = Module.HEAP16[pstStreamInfo >> 1]; //封装类型
+        let nVideoformat = Module.HEAP16[pstStreamInfo + 2 >> 1];//视频编码类型
+        let nAudioformat = Module.HEAP16[pstStreamInfo + 4 >> 1];//音频编码类型
+        let nAudiochannels = Module.HEAP16[pstStreamInfo + 6 >> 1]; //音频通道数
+        let nAudiobitspersample = Module.HEAP32[pstStreamInfo + 8 >> 2];//音频样位率
+        let nAudiosamplesrate = Module.HEAP32[pstStreamInfo + 12 >> 2];//音频采样率
+        let nAudiobitrate = Module.HEAP32[pstStreamInfo + 16 >> 2];//音频比特率,单位:bit
+		//console.log("nSystemformat:" + nSystemformat + ",nVideoformat:" + nVideoformat + ",nAudioformat:" + nAudioformat + ",nAudiochannels:" + nAudiochannels + ",nAudiobitspersample:" + nAudiobitspersample + ",nAudiosamplesrate:" + nAudiosamplesrate + ",nAudiobitrate:" + nAudiobitrate);
+        postMessage({ 'function': "StreamInfoCallBack", 'nSystemformat': nSystemformat, 'nVideoformat': nVideoformat, 'nAudioformat': nAudioformat, 'nAudiochannels': nAudiochannels, 'nAudiobitspersample': nAudiobitspersample, 'nAudiosamplesrate': nAudiosamplesrate, 'nAudiobitrate': nAudiobitrate});
+	}
+
+    onmessage = function (event) {
+        var eventData = event.data;
+        var res = 0;
+        switch (eventData.command) {
+            case "importScripts":
+                const decodebase = eventData.data + "Decoder.js"
+                importScripts(decodebase);
+                Module.addOnPostRun(function () {
+                    postMessage({ 'function': "loaded" });
+                });
+                break;
+            case "printLog":
+                let downloadFlag = eventData.data;
+                if (downloadFlag === true) {
+                    bWorkerPrintLog = true;
+                    res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
+                }
+                else {
+                    bWorkerPrintLog = false;
+                    res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
+                }
+
+                if (res !== PLAYM4_OK) {
+                    console.log("DecodeWorker.js: PlayerSDK print log failed,res" + res);
+                    postMessage({ 'function': "printLog", 'errorCode': res });
+                }
+                break;
+            case "SetPlayPosition":
+                let nFrameNumOrTime = eventData.data;
+                let enPosType = eventData.type;
+                // res = Module._SetPlayPosition(nFrameNumOrTime,enPosType);
+                // if (res !== PLAYM4_OK)
+                // {
+                //     postMessage({'function': "SetPlayPosition", 'errorCode': res});
+                //     return;
+                // }
+                // //有没有buffer需要清除
+
+                break;
+            case "SetStreamOpenMode":
+                //获取端口号
+                g_nPort = Module._GetPort();
+                //设置流打开模式
+                iStreamMode = eventData.data;
+                res = Module._SetStreamOpenMode(g_nPort, iStreamMode);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetStreamOpenMode", 'errorCode': res });
+                    return;
+                }
+                bOpenMode = true;
+                break;
+
+            case "OpenStream":
+                // 接收到的数据
+                var iHeadLen = eventData.dataSize;
+                var pHead = Module._malloc(iHeadLen + 4);
+                if (pHead === null) {
+                    return;
+                }
+                var aHead = Module.HEAPU8.subarray(pHead, pHead + iHeadLen);
+                aHead.set(new Uint8Array(eventData.data));
+                res = Module._OpenStream(g_nPort, pHead, iHeadLen, eventData.bufPoolSize);
+                postMessage({ 'function': "OpenStream", 'errorCode': res });
+                if (res !== PLAYM4_OK) {
+                    //释放内存
+                    Module._free(pHead);
+                    pHead = null;
+                    return;
+                }
+                bOpenStream = true;
+                break;
+            case "Play":
+                let resP = Module._Play(g_nPort);
+                if (resP !== PLAYM4_OK) {
+                    return;
+                }
+                break;
+            case "InputData":
+                // 接收到的数据
+                var iLen = eventData.dataSize;
+                if (iLen > 0) {
+                    if (pInputData == null || iLen > inputBufferSize) {
+                        if (pInputData != null) {
+                            Module._free(pInputData);
+                            pInputData = null;
+                        }
+                        if (iLen > inputBufferSize) {
+                            inputBufferSize = iLen;
+                        }
+
+                        pInputData = Module._malloc(inputBufferSize);
+                        if (pInputData === null) {
+                            return;
+                        }
+                    }
+
+                    var inputData = new Uint8Array(eventData.data);
+                    // var aInputData = Module.HEAPU8.subarray(pInputData, pInputData + iLen);
+                    // aInputData.set(inputData);
+                    Module.writeArrayToMemory(inputData, pInputData);
+                    inputData = null;
+                    res = Module._InputData(g_nPort, pInputData, iLen);
+                    if (res !== PLAYM4_OK) {
+                        let errorCode = Module._GetLastError(g_nPort);
+                        let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
+                        postMessage({ 'function': "InputData", 'errorCode': errorCode, "sourceRemain": sourceRemain });
+                    }
+                    //Module._free(pInputData);
+                    //pInputData = null;
+                } else {
+                    let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
+                    if (sourceRemain == 0) {
+                        postMessage({ 'function': "InputData", 'errorCode': PLAYM4_NEED_MORE_DATA });
+                        return;
+                    }
+                }
+
+                /////////////////////
+                // if (funGetFrameData === null) {
+                //     funGetFrameData = Module.cwrap('GetFrameData', 'number');
+                // }
+
+                while (bOpenMode && bOpenStream) {
+
+                    var ret = getFrameData();
+                    // 直到获取视频帧或数据不足为止
+                    if (PLAYM4_VIDEO_FRAME === ret || PLAYM4_NEED_MORE_DATA === ret || PLAYM4_ORDER_ERROR === ret)//PLAYM4_VIDEO_FRAME === ret ||  || PLAYM4_NEED_NEET_LOOP === ret
+                    {
+                        break;
+                    }
+                }
+                break;
+
+            case "SetSecretKey":
+                var keyLen = eventData.nKeyLen;
+                var pKeyData = Module._malloc(keyLen);
+                if (pKeyData === null) {
+                    return;
+                }
+                var nKeySize = eventData.data.length
+                var bufData = stringToBytes(eventData.data);
+                var aKeyData = Module.HEAPU8.subarray(pKeyData, pKeyData + keyLen);
+                let u8array = new Uint8Array(keyLen);
+                aKeyData.set(u8array, 0);
+                aKeyData.set(new Uint8Array(bufData));
+                aKeyData = null;
+                u8array = null;
+
+                res = Module._SetSecretKey(g_nPort, eventData.nKeyType, pKeyData, keyLen);//, nKeySize
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetSecretKey", 'errorCode': res });
+                    Module._free(pKeyData);
+                    pKeyData = null;
+                    return;
+                }
+
+                Module._free(pKeyData);
+                pKeyData = null;
+                break;
+
+            case "GetBMP":
+                var nBMPWidth = eventData.width;
+                var nBMPHeight = eventData.height;
+                var pYUVData = eventData.data;
+                var nYUVSize = nBMPWidth * nBMPHeight * 3 / 2;
+                var oBMPCropRect = {
+                    left: eventData.left,
+                    top: eventData.top,
+                    right: eventData.right,
+                    bottom: eventData.bottom
+                };
+
+                var pDataYUV = Module._malloc(nYUVSize);
+                if (pDataYUV === null) {
+                    return;
+                }
+
+                Module.writeArrayToMemory(new Uint8Array(pYUVData, 0, nYUVSize), pDataYUV);
+
+                // 分配BMP空间
+                var nBmpSize = nBMPWidth * nBMPHeight * 4 + 60;
+                var pBmpData = Module._malloc(nBmpSize);
+                var pBmpSize = Module._malloc(4);
+                if (pBmpData === null || pBmpSize === null) {
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+
+                    if (pBmpData != null) {
+                        Module._free(pBmpData);
+                        pBmpData = null;
+                    }
+
+                    if (pBmpSize != null) {
+                        Module._free(pBmpSize);
+                        pBmpSize = null;
+                    }
+                    return;
+                }
+
+                //Module._memset(pBmpSize, nBmpSize, 4); // 防止bmp截图出现输入数据过大的错误码
+                Module.setValue(pBmpSize, nBmpSize, "i32");
+                res = Module._GetBMP(g_nPort, pDataYUV, nYUVSize, pBmpData, pBmpSize,
+                    oBMPCropRect.left, oBMPCropRect.top, oBMPCropRect.right, oBMPCropRect.bottom);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "GetBMP", 'errorCode': res });
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+                    Module._free(pBmpData);
+                    pBmpData = null;
+                    Module._free(pBmpSize);
+                    pBmpSize = null;
+                    return;
+                }
+
+                // 获取BMP图片大小
+                var nBmpDataSize = Module.getValue(pBmpSize, "i32");
+
+                // 获取BMP图片数据
+                var aBmpData = new Uint8Array(nBmpDataSize);
+                aBmpData.set(Module.HEAPU8.subarray(pBmpData, pBmpData + nBmpDataSize));
+
+                postMessage({ 'function': "GetBMP", 'data': aBmpData, 'errorCode': res }, [aBmpData.buffer]);
+                aBmpData = null;
+                if (pDataYUV != null) {
+                    Module._free(pDataYUV);
+                    pDataYUV = null;
+                }
+                if (pBmpData != null) {
+                    Module._free(pBmpData);
+                    pBmpData = null;
+                }
+                if (pBmpSize != null) {
+                    Module._free(pBmpSize);
+                    pBmpSize = null;
+                }
+                break;
+
+            case "GetJPEG":
+                var nJpegWidth = eventData.width;
+                var nJpegHeight = eventData.height;
+                var pYUVData1 = eventData.data;
+                var nYUVSize1 = nJpegWidth * nJpegHeight * 3 / 2;
+                var oJpegCropRect = {
+                    left: eventData.left,
+                    top: eventData.top,
+                    right: eventData.right,
+                    bottom: eventData.bottom
+                };
+
+                var pDataYUV1 = Module._malloc(nYUVSize1);
+                if (pDataYUV1 === null) {
+                    return;
+                }
+
+                Module.writeArrayToMemory(new Uint8Array(pYUVData1, 0, nYUVSize1), pDataYUV1);
+
+                // 分配JPEG空间
+                var pJpegData = Module._malloc(nYUVSize1);
+                var pJpegSize = Module._malloc(4);
+                if (pJpegData === null || pJpegSize === null) {
+                    if (pJpegData != null) {
+                        Module._free(pJpegData);
+                        pJpegData = null;
+                    }
+
+                    if (pJpegSize != null) {
+                        Module._free(pJpegSize);
+                        pJpegSize = null;
+                    }
+
+                    if (pDataYUV1 != null) {
+                        Module._free(pDataYUV1);
+                        pDataYUV1 = null;
+                    }
+                    return;
+                }
+
+                Module.setValue(pJpegSize, nJpegWidth * nJpegHeight * 2, "i32");    // JPEG抓图,输入缓冲长度不小于当前帧YUV大小
+
+                res = Module._GetJPEG(g_nPort, pDataYUV1, nYUVSize1, pJpegData, pJpegSize,
+                    oJpegCropRect.left, oJpegCropRect.top, oJpegCropRect.right, oJpegCropRect.bottom);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "GetJPEG", 'errorCode': res });
+                    if (pJpegData != null) {
+                        Module._free(pJpegData);
+                        pJpegData = null;
+                    }
+
+                    if (pJpegSize != null) {
+                        Module._free(pJpegSize);
+                        pJpegSize = null;
+                    }
+
+                    if (pDataYUV1 != null) {
+                        Module._free(pDataYUV1);
+                        pDataYUV1 = null;
+                    }
+                    return;
+                }
+
+                // 获取JPEG图片大小
+                var nJpegSize = Module.getValue(pJpegSize, "i32");
+
+                // 获取JPEG图片数据
+                var aJpegData = new Uint8Array(nJpegSize);
+                aJpegData.set(Module.HEAPU8.subarray(pJpegData, pJpegData + nJpegSize));
+
+                postMessage({ 'function': "GetJPEG", 'data': aJpegData, 'errorCode': res }, [aJpegData.buffer]);
+
+                nJpegSize = null;
+                aJpegData = null;
+
+                if (pDataYUV1 != null) {
+                    Module._free(pDataYUV1);
+                    pDataYUV1 = null;
+                }
+                if (pJpegData != null) {
+                    Module._free(pJpegData);
+                    pJpegData = null;
+                }
+                if (pJpegSize != null) {
+                    Module._free(pJpegSize);
+                    pJpegSize = null;
+                }
+                break;
+
+            case "SetDecodeFrameType":
+                var nFrameType = eventData.data;
+                res = Module._SetDecodeFrameType(g_nPort, nFrameType);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "SetDecodeFrameType", 'errorCode': res });
+                    return;
+                }
+                break;
+            case "CloseStream":
+                //stop
+                let resS = Module._Stop(g_nPort);
+                if (resS !== PLAYM4_OK) {
+                    postMessage({ 'function': "Stop", 'errorCode': res });
+                    return;
+                }
+                //closeStream
+                res = Module._CloseStream(g_nPort);
+                if (res !== PLAYM4_OK) {
+                    postMessage({ 'function': "CloseStream", 'errorCode': res });
+                    return;
+                }
+                //freePort
+                let resF = Module._FreePort(g_nPort);
+                if (resF !== PLAYM4_OK) {
+                    postMessage({ 'function': "FreePort", 'errorCode': res });
+                    return;
+                }
+                if (pInputData != null) {
+                    Module._free(pInputData);
+                    pInputData = null;
+                }
+                break;
+            case "PlaySound":
+                let resPS = Module._PlaySound(g_nPort);
+                if (resPS !== PLAYM4_OK) {
+                    console.log("PlaySound failed");
+                    return;
+                }
+                break;
+            case "StopSound":
+                let resSS = Module._StopSound();
+                if (resSS !== PLAYM4_OK) {
+                    console.log("StopSound failed");
+                    return;
+                }
+                break;
+            case "SetVolume":
+                let resSV = Module._SetVolume(g_nPort, eventData.volume);
+                if (resSV !== PLAYM4_OK) {
+                    console.log("Audio SetVolume failed");
+                    return;
+                }
+                break;
+            case "GetVolume":
+                let volume = Module._GetVolume();
+                if (volume > 0) {
+                    postMessage({ 'function': "GetVolume", 'volume': volume });
+                }
+                else {
+                    console.log("Audio GetVolume failed");
+                    return;
+                }
+                break;
+            case "OnlyPlaySound":
+                let resOPS = Module._OnlyPlaySound(g_nPort);
+                if (resOPS !== PLAYM4_OK) {
+                    console.log("OnlyPlaySound failed");
+                    return;
+                }
+                break;
+            case "Pause":
+                let resPa = Module._Pause(g_nPort, eventData.bPlay);
+                if (resPa !== PLAYM4_OK) {
+                    console.log("Pause failed");
+                    return;
+                }
+            case "PlayRate":
+                Module._SetPlayRate(g_nPort, eventData.playRate);
+                break;
+            case "SetIFrameDecInterval":
+                Module._SetIFrameDecInterval(g_nPort, eventData.data);
+                break;
+            case "SetLostFrameMode":
+                Module._SetLostFrameMode(g_nPort, eventData.data, 0);
+                break;
+            case "SetDemuxModel":
+                let resSDM = Module._SetDemuxModel(g_nPort, eventData.nIdemuxType, eventData.bTrue);
+                break;
+            case "SkipErrorData":
+                Module._SkipErrorData(g_nPort, eventData.bSkip);
+                break;
+            case "SetDecodeERC":
+                Module._SetDecodeERC(g_nPort, eventData.nLevel);
+                break;
+            case "SetANRParam":
+                Module._SetANRParam(g_nPort, eventData.nEnable, eventData.nANRLevel);
+                break;
+            case "SetResampleValue":
+                Module._SetResampleValue(g_nPort, eventData.nEnable, eventData.resampleValue);
+                break;
+            case "GetLastError":
+                let errorCode = Module._GetLastError(g_nPort);
+                postMessage({ 'function': "GetLastError", 'errorCode': errorCode });
+                break;
+            case "SetGlobalBaseTime":
+                Module._SetGlobalBaseTime(g_nPort, eventData.year, eventData.month, eventData.day, eventData.hour, eventData.min, eventData.sec, eventData.ms);
+                break;
+            case "SetRunTimeInfoCB":
+                Module._SetRunTimeInfoCallBackEx(g_nPort, eventData.nModuleType, 0);
+                break;
+			case "SetStreamInfoCB":
+                Module._SetStreamInfoCallBack(g_nPort, eventData.nType, 0);
+                break;
+			case "ResetBuffer":
+                Module._JSPlayM4_ResetBuffer(g_nPort, eventData.type);
+                break;
+            default:
+                break;
+        }
+    };
+
+    function getOSDTime(oFrameInfo) {
+        var iYear = oFrameInfo.year;
+        var iMonth = oFrameInfo.month;
+        var iDay = oFrameInfo.day;
+        var iHour = oFrameInfo.hour;
+        var iMinute = oFrameInfo.minute;
+        var iSecond = oFrameInfo.second;
+        var iMiSecond = oFrameInfo.misecond
+
+        if (iMonth < 10) {
+            iMonth = "0" + iMonth;
+        }
+        if (iDay < 10) {
+            iDay = "0" + iDay;
+        }
+        if (iHour < 10) {
+            iHour = "0" + iHour;
+        }
+        if (iMinute < 10) {
+            iMinute = "0" + iMinute;
+        }
+        if (iSecond < 10) {
+            iSecond = "0" + iSecond;
+        }
+        let osdTime = {};
+
+        osdTime.year = iYear;
+        osdTime.month = iMonth;
+        osdTime.week = 0;
+        osdTime.day = iDay;
+        osdTime.hour = iHour;
+        osdTime.minute = iMinute;
+        osdTime.second = iSecond;
+        osdTime.milliseconds = iMiSecond;
+        return osdTime;
+        //return iYear + "-" + iMonth + "-" + iDay + " " + iHour + ":" + iMinute + ":" + iSecond;
+    }
+    // 获取帧数据
+    function getFrameData() {
+        // function getFrameData() {
+        // 获取帧数据
+        var res = Module._GetFrameData();
+        //var res = fun();
+        if (res === PLAYM4_OK) {
+            var iFrameInfo = Module._GetFrameInfo();
+            let oFrameInfo = {};
+            oFrameInfo.frameType = Module.HEAP32[iFrameInfo >> 2];
+            oFrameInfo.frameSize = Module.HEAP32[iFrameInfo + 4 >> 2];
+            oFrameInfo.width = Module.HEAP32[iFrameInfo + 8 >> 2];
+            oFrameInfo.height = Module.HEAP32[iFrameInfo + 12 >> 2];
+            oFrameInfo.timeStamp = Module.HEAP32[iFrameInfo + 16 >> 2];
+            oFrameInfo.frameRate = Module.HEAP32[iFrameInfo + 20 >> 2];
+            oFrameInfo.bitsPerSample = Module.HEAP32[iFrameInfo + 24 >> 2];
+            oFrameInfo.samplesPerSec = Module.HEAP32[iFrameInfo + 28 >> 2];
+            oFrameInfo.channels = Module.HEAP32[iFrameInfo + 32 >> 2];
+            oFrameInfo.frameNum = Module.HEAP32[iFrameInfo + 36 >> 2];
+
+            oFrameInfo.cropLeft = Module.HEAP32[iFrameInfo + 40 >> 2];
+            oFrameInfo.cropRight = Module.HEAP32[iFrameInfo + 44 >> 2];
+            oFrameInfo.cropTop = Module.HEAP32[iFrameInfo + 48 >> 2];
+            oFrameInfo.cropBottom = Module.HEAP32[iFrameInfo + 52 >> 2];
+
+            oFrameInfo.year = Module.HEAP16[iFrameInfo + 64 >> 1];
+            oFrameInfo.month = Module.HEAP16[iFrameInfo + 66 >> 1];
+            oFrameInfo.day = Module.HEAP16[iFrameInfo + 68 >> 1];
+            oFrameInfo.hour = Module.HEAP16[iFrameInfo + 70 >> 1];
+            oFrameInfo.minute = Module.HEAP16[iFrameInfo + 72 >> 1];
+            oFrameInfo.second = Module.HEAP16[iFrameInfo + 74 >> 1];
+            oFrameInfo.misecond = Module.HEAP16[iFrameInfo + 76 >> 1];
+            switch (oFrameInfo.frameType) {
+                case AUDIO_TYPE:
+                    var iSize = oFrameInfo.frameSize;
+                    if (0 === iSize) {
+                        return -1;
+                    }
+                    var pPCM = Module._GetFrameBuffer();
+                    // var audioBuf = new ArrayBuffer(iSize);
+                    var aPCMData = new Uint8Array(iSize);
+                    aPCMData.set(Module.HEAPU8.subarray(pPCM, pPCM + iSize));
+                    if (bWorkerPrintLog) {
+                        console.log("<<<Worker: audio media Info: nSise:" + oFrameInfo.frameSize + ",nSampleRate:" + oFrameInfo.samplesPerSec + ',channel:' + oFrameInfo.channels + ',bitsPerSample:' + oFrameInfo.bitsPerSample);
+                    }
+                    postMessage({
+                        'function': "GetFrameData", 'type': "audioType", 'data': aPCMData.buffer,
+                        'frameInfo': oFrameInfo, 'errorCode': res
+                    }, [aPCMData.buffer]);
+
+                    oFrameInfo = null;
+                    pPCM = null;
+                    aPCMData = null;
+                    return PLAYM4_AUDIO_FRAME;
+
+                case VIDEO_TYPE:
+                    var szOSDTime = getOSDTime(oFrameInfo);
+
+                    var iWidth = oFrameInfo.width;
+                    var iHeight = oFrameInfo.height;
+
+                    var iYUVSize = iWidth * iHeight * 3 / 2;
+                    if (0 === iYUVSize) {
+                        return -1;
+                    }
+
+                    var pYUV = Module._GetFrameBuffer();
+
+                    // 图像数据渲染后压回,若从主码流切到子码流,存在数组大小与图像大小不匹配现象
+                    var aYUVData = new Uint8Array(iYUVSize);
+                    aYUVData.set(Module.HEAPU8.subarray(pYUV, pYUV + iYUVSize));
+                    if (bWorkerPrintLog) {
+                        console.log("<<<Worker: video media Info: Width:" + oFrameInfo.width + ",Height:" + oFrameInfo.height + ",timeStamp:" + oFrameInfo.timeStamp);
+                    }
+
+                    postMessage({
+                        'function': "GetFrameData", 'type': "videoType", 'data': aYUVData.buffer,
+                        'dataLen': aYUVData.length, 'osd': szOSDTime, 'frameInfo': oFrameInfo, 'errorCode': res
+                    }, [aYUVData.buffer]);
+
+                    oFrameInfo = null;
+                    pYUV = null;
+                    aYUVData = null;
+                    return PLAYM4_VIDEO_FRAME;
+
+                case PRIVT_TYPE:
+                    postMessage({
+                        'function': "GetFrameData", 'type': "", 'data': null,
+                        'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
+                    });
+                    return PLAYM4_SYS_NOT_SUPPORT;
+
+                default:
+                    postMessage({
+                        'function': "GetFrameData", 'type': "", 'data': null,
+                        'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
+                    });
+                    return PLAYM4_SYS_NOT_SUPPORT;
+            }
+        }
+        else {
+            let errorCode = Module._GetLastError(g_nPort);
+            //解码失败返回裸数据
+            if (PLAYM4_DECODE_ERROR === errorCode) {
+                var rawInfo = Module._GetRawDataInfo();
+                var pRawData = Module._GetRawDataBuffer();
+                var aRawData = new Uint8Array(rawInfo.isize);
+                aRawData.set(Module.HEAPU8.subarray(pRawData, pRawData + rawInfo.isize));
+                postMessage({
+                    'function': "GetRawData", 'type': "", 'data': aRawData.buffer,
+                    'rawDataLen': rawInfo.isize, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
+                });
+                rawInfo = null;
+                pRawData = null;
+                aRawData = null;
+            }
+            //需要更多数据
+            if (PLAYM4_NEED_MORE_DATA === errorCode || PLAYM4_SYS_NOT_SUPPORT === errorCode || PLAYM4_NEED_NEET_LOOP === errorCode) {
+                postMessage({
+                    'function': "GetFrameData", 'type': "", 'data': null,
+                    'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
+                });
+            }
+            return errorCode;
+        }
+    }
+
+    // 开始计算时间
+    function startTime() {
+        return new Date().getTime();
+    }
+
+    // 结束计算时间
+    function endTime() {
+        return new Date().getTime();
+    }
+
+    // 字母字符串转byte数组
+    function stringToBytes(str) {
+        var ch, st, re = [];
+        for (var i = 0; i < str.length; i++) {
+            ch = str.charCodeAt(i);  // get char
+            st = [];                 // set up "stack"
+            do {
+                st.push(ch & 0xFF);  // push byte to stack
+                ch = ch >> 8;          // shift value down by 1 byte
+            }
+            while (ch);
+            // add stack contents to result
+            // done because chars have "wrong" endianness
+            re = re.concat(st.reverse());
+        }
+        // return an array of bytes
+        return re;
+    }
+})();

binární
public/h5player/playctrl1simd/Decoder.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/playctrl2/Decoder.js


binární
public/h5player/playctrl2/Decoder.wasm


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/playctrl2/Decoder.worker.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/playctrl3/Decoder.js


binární
public/h5player/playctrl3/Decoder.wasm


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/playctrl3/Decoder.worker.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 7 - 0
public/h5player/talk/AudioInterCom.js


binární
public/h5player/talk/AudioInterCom.wasm


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 8 - 0
public/h5player/talkW/AudioInterCom.js


binární
public/h5player/talkW/AudioInterCom.wasm


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/h5player/talkW/AudioInterCom.worker.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2668 - 0
public/h5player/transform/libSystemTransform.js


binární
public/h5player/transform/libSystemTransform.wasm


+ 126 - 0
public/h5player/transform/systemTransform-worker.js

@@ -0,0 +1,126 @@
+importScripts('libSystemTransform.js');
+    const RECORDRTP = 0;  //录制一份未经过转封装的码流原始数据,用于定位问题
+    let dataType = 1;
+    
+    // 转封装库回调函数
+    self.STCallBack = function (fileIndex,indexLen, data, dataLen)
+    {
+        //stFrameInfo的类型见DETAIL_FRAME_INFO
+		let stFrameInfo = Module._GetDetialFrameInfo();
+        let nIsMp4Index = stFrameInfo.nIsMp4Index;
+		//console.log("FrameType is " , stFrameInfo);	
+		//console.log("nIsMp4Index is " + nIsMp4Index);
+        //debugger
+        var pData = null;
+        pData = new Uint8Array(dataLen);
+        pData.set(Module.HEAPU8.subarray(data, data + dataLen));
+        if (dataType === 1) {
+            if (pData[0] == 0x49 && pData[1] == 0x4d && pData[2] == 0x4b && pData[3] == 0x48) {//码流头丢掉
+                return;
+            }
+            postMessage({type: "outputData", buf: pData, dType: 1});
+            dataType = 2;
+        } else {
+            if (nIsMp4Index) {
+                postMessage({type: "outputData", buf: pData, dType: 6}); //6:索引类型
+            } else {
+                postMessage({type: "outputData", buf: pData, dType: 2}); //2:码流
+            }
+        }
+
+        //stFrameInfo的类型见DETAIL_FRAME_INFO
+		//let stFrameInfo = Module._GetDetialFrameInfo();
+		//let stFrameType = stFrameInfo.nFrameType;
+		//let nFrameNum = stFrameInfo.nFrameNum;
+		//let nTimeStamp = stFrameInfo.nTimeStamp;
+        //let nIsMp4Index = stFrameInfo.nIsMp4Index;
+		
+		//console.log("FrameType is " + stFrameType);	
+		//console.log("nIsMp4Index is " + nIsMp4Index);	
+        
+    }
+
+    // self.Module = { memoryInitializerRequest: loadMemInitFile(), TOTAL_MEMORY: 128*1024*1024 };
+    // importScripts('SystemTransform.js');
+
+    self.Module['onRuntimeInitialized'] = function (){
+        postMessage({type: "loaded"});
+    }
+    onmessage = function (e) {
+        var data = e.data;
+        if ("create" === data.type) {
+            if (RECORDRTP) {
+                postMessage({type: "created"});
+                postMessage({type: "outputData", buf: data.buf, dType: 1});
+            } else {
+                var iHeadLen = data.len;
+                var pHead = Module._malloc(iHeadLen);
+    
+                self.writeArrayToMemory(new Uint8Array(data.buf), pHead);
+                var iTransType = data.packType;//目标格式
+                var iRet = Module._CreatHandle(pHead, iTransType, 4096);
+                if (iRet != 0) {
+                    console.log("_CreatHandle failed!" + iRet);
+                } else {
+                    iRet = Module._SysTransRegisterDataCallBack();			
+                    if(iRet != 0)
+                    {
+                        console.log("_SysTransRegisterDataCallBack Failed:" + iRet);
+                    }
+                    if (iTransType === 5) {//转mp4支持私有帧打入
+                        iRet = Module._SysTransEnableCapacity(7)
+                        if(iRet != 0)
+                        {
+                            console.log("_SysTransSetGlobalTime Failed:" + iRet);
+                        }
+                    }
+                    iRet = Module._SysTransStart(null, null);
+                    if(iRet != 0)
+                    {
+                        console.log("_SysTransStart Failed:" + iRet);
+                    }
+                    postMessage({type: "created"});
+                }
+            }
+
+        } else if ("inputData" === data.type) {
+            if (RECORDRTP) {
+                var aFileData = new Uint8Array(data.buf);  // 拷贝一份
+                var iBufferLen = aFileData.length;
+                var szBufferLen = iBufferLen.toString(16);
+                if (szBufferLen.length === 1) {
+                    szBufferLen = "000" + szBufferLen;
+                } else if (szBufferLen.length === 2) {
+                    szBufferLen = "00" + szBufferLen;
+                } else if (szBufferLen.length === 3) {
+                    szBufferLen = "0" + szBufferLen;
+                }
+                var aData = [0, 0, parseInt(szBufferLen.substring(0, 2), 16), parseInt(szBufferLen.substring(2, 4), 16)];
+                for(var iIndex = 0, iDataLength = aFileData.length; iIndex < iDataLength; iIndex++) {
+                    aData[iIndex + 4] = aFileData[iIndex]
+                }
+                var dataUint8 = new Uint8Array(aData);
+                postMessage({type: "outputData", buf: dataUint8.buffer, dType: 2});
+            } else {
+                let pInputDataBuf = Module._malloc(data.len);
+                var idataLen = data.len;
+                self.writeArrayToMemory(new Uint8Array(data.buf), pInputDataBuf);
+                    // 输入数据,每次最多2m
+                let pp = Module._SysTransInputData(0, pInputDataBuf, idataLen);
+                if(pp != 0) {
+                    //console.log("InputData Failed:" + pp);
+                }
+                Module._free(pInputDataBuf);
+            }
+        } else if ("release" === data.type) {
+            var iRet = Module._SysTransStop();
+            if (iRet != 0) {
+                console.log("_SysTransStop Failed:", iRet);
+            }
+            Module._SysTransRelease();
+            if (iRet != 0) {
+                console.log("_SysTransRelease Failed:", iRet);
+            }
+            close();
+        }
+    };

+ 43 - 0
src/api/pms/hikvision/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+export interface HikvisionRegionNode {
+  indexCode: string
+  name: string
+  nodeType: 'region'
+  parentIndexCode?: string
+  available?: boolean
+  leaf: boolean
+  children: HikvisionTreeNode[]
+}
+
+export interface HikvisionCameraNode {
+  indexCode: string
+  name: string
+  nodeType: 'camera'
+  parentIndexCode?: string
+  available?: boolean
+  onlineStatus?: boolean
+}
+
+export type HikvisionTreeNode = HikvisionRegionNode | HikvisionCameraNode
+
+export const HikvisionApi = {
+  getTreeNodes: (regionIndexCode: string): Promise<HikvisionTreeNode[]> => {
+    return request.get({
+      url: '/vms/tree',
+      params: { regionIndexCode }
+    })
+  },
+
+  /**
+   * 获取监控点的 H5player 实时预览地址。
+   *
+   * 后端接口待实现,约定 CommonResult.data 直接返回可供 JS_Play 使用的 URL 字符串。
+   */
+  getPreviewUrl: (cameraIndexCode: string): Promise<string> => {
+    return request.get({
+      url: '/vms/preview-url',
+      params: { cameraIndexCode }
+    })
+  }
+}

+ 1 - 0
src/assets/icons/grid-16.svg

@@ -0,0 +1 @@
+<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><clipPath id="clip1_20937"><rect width="24.000000" height="24.000000" fill="white" fill-opacity="0"></rect></clipPath></defs><rect width="24.000000" height="24.000000" fill="currentColor" fill-opacity="0"></rect><rect width="24.000000" height="24.000000" fill-opacity="1.000000"></rect><g clip-path="url(#clip1_20937)"><path d="M1 3L22 3C22.5523 3 23 3.44771 23 4L23 20C23 20.5523 22.5523 21 22 21L1 21C0.447723 21 0 20.5523 0 20L0 4C0 3.44771 0.447723 3 1 3ZM22 4L1 4L1 20L22 20L22 4ZM2 5L6 5L6 8L2 8L2 5ZM7 5L11 5L11 8L7 8L7 5ZM12 5L16 5L16 8L12 8L12 5ZM17 5L21 5L21 8L17 8L17 5ZM2 9L6 9L6 11L2 11L2 9ZM7 9L11 9L11 11L7 11L7 9ZM12 9L16 9L16 11L12 11L12 9ZM17 9L21 9L21 11L17 11L17 9ZM2 12L6 12L6 15L2 15L2 12ZM7 12L11 12L11 15L7 15L7 12ZM12 12L16 12L16 15L12 15L12 12ZM17 12L21 12L21 15L17 15L17 12ZM2 16L6 16L6 19L2 19L2 16ZM7 16L11 16L11 19L7 19L7 16ZM12 16L16 16L16 19L12 19L12 16ZM17 16L21 16L21 19L17 19L17 16Z" clip-rule="evenodd" fill="currentColor" fill-opacity="0.701961" fill-rule="evenodd"></path></g></svg>

+ 1 - 0
src/assets/icons/grid-four.svg

@@ -0,0 +1 @@
+<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><clipPath id="clip1_20933"><rect width="24.000000" height="24.000000" fill="white" fill-opacity="0"></rect></clipPath></defs><rect width="24.000000" height="24.000000" fill="currentColor" fill-opacity="0"></rect><rect width="24.000000" height="24.000000" fill-opacity="1.000000"></rect><g clip-path="url(#clip1_20933)"><path d="M22 3L1 3C0.447723 3 0 3.44771 0 4L0 20C0 20.5523 0.447723 21 1 21L22 21C22.5523 21 23 20.5523 23 20L23 4C23 3.44771 22.5523 3 22 3ZM1 4L22 4L22 20L1 20L1 4ZM2 5L11 5L11 11L2 11L2 5ZM12 5L21 5L21 11L12 11L12 5ZM2 12L11 12L11 19L2 19L2 12ZM21 12L12 12L12 19L21 19L21 12Z" clip-rule="evenodd" fill="currentColor" fill-opacity="0.701961" fill-rule="evenodd"></path></g></svg>

+ 1 - 0
src/assets/icons/grid-nine.svg

@@ -0,0 +1 @@
+<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><clipPath id="clip1_20935"><rect width="24.000000" height="24.000000" fill="white" fill-opacity="0"></rect></clipPath></defs><rect width="24.000000" height="24.000000" fill="currentColor" fill-opacity="0"></rect><rect width="24.000000" height="24.000000" fill-opacity="1.000000"></rect><g clip-path="url(#clip1_20935)"><path d="M1 3L22 3C22.5523 3 23 3.44771 23 4L23 20C23 20.5523 22.5523 21 22 21L1 21C0.447723 21 0 20.5523 0 20L0 4C0 3.44771 0.447723 3 1 3ZM22 4L1 4L1 20L22 20L22 4ZM2 5L8 5L8 9L2 9L2 5ZM9 5L14 5L14 9L9 9L9 5ZM15 5L21 5L21 9L15 9L15 5ZM2 10L8 10L8 14L2 14L2 10ZM9 10L14 10L14 14L9 14L9 10ZM15 10L21 10L21 14L15 14L15 10ZM2 15L8 15L8 19L2 19L2 15ZM9 15L14 15L14 19L9 19L9 15ZM15 15L21 15L21 19L15 19L15 15Z" clip-rule="evenodd" fill="currentColor" fill-opacity="0.701961" fill-rule="evenodd"></path></g></svg>

+ 1 - 0
src/assets/icons/grid-one.svg

@@ -0,0 +1 @@
+<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><clipPath id="clip1_20931"><rect width="24.000000" height="24.000000" fill="white" fill-opacity="0"></rect></clipPath></defs><rect width="24.000000" height="24.000000" fill="currentColor" fill-opacity="0"></rect><rect width="24.000000" height="24.000000" fill-opacity="1.000000"></rect><g clip-path="url(#clip1_20931)"><path d="M1 3L22 3C22.5523 3 23 3.44771 23 4L23 20C23 20.5523 22.5523 21 22 21L1 21C0.447723 21 0 20.5523 0 20L0 4C0 3.44771 0.447723 3 1 3ZM22 4L1 4L1 20L22 20L22 4ZM21 5L2 5L2 19L21 19L21 5Z" clip-rule="evenodd" fill="currentColor" fill-opacity="0.701961" fill-rule="evenodd"></path></g></svg>

+ 146 - 0
src/utils/hikvisionH5Player.ts

@@ -0,0 +1,146 @@
+/**
+ * H5player 的分屏参数表示每行/每列的窗口数量,而不是窗口总数。
+ * 例如传入 2 表示 2×2(4 窗口),传入 4 表示 4×4(16 窗口)。
+ * 开发指南限定该参数范围为 1~4,使用联合类型可以在编译阶段阻止非法值。
+ */
+export type HikvisionSplitColumns = 1 | 2 | 3 | 4
+
+/** 创建 H5player 实例时使用的配置。 */
+export interface HikvisionPlayerOptions {
+  /** 播放器挂载容器的 id,必须以英文字母开头。 */
+  szId: string
+  /** SDK 资源目录,必须与 h5player.min.js 所在目录一致。 */
+  szBasePath: string
+  /** 实例允许使用的最大分屏规格,4 表示最大支持 4×4。 */
+  iMaxSplit?: HikvisionSplitColumns
+  /** 实例创建时显示的分屏规格,2 表示默认显示 2×2。 */
+  iCurrentSplit?: HikvisionSplitColumns
+  /** 是否输出 SDK 调试日志。 */
+  openDebug?: boolean
+  /** 是否启用 MSE Worker;开发指南建议分辨率高于 1080P 时开启。 */
+  mseWorkerEnable?: boolean
+  /** 是否允许双击播放器进入或退出全屏。 */
+  bSupporDoubleClickFull?: boolean
+  oStyle?: {
+    /** SDK 内部当前选中窗口的边框颜色。 */
+    borderSelect?: string
+  }
+}
+
+/**
+ * JS_SetWindowControlCallback 支持的事件。
+ * 这里只声明当前预览页面实际需要的事件,后续接入声音、抓图或回放时再按指南扩展。
+ */
+export interface HikvisionPlayerEvents {
+  /** 取流、解码或播放器内部异常。 */
+  pluginErrorHandler?: (windowIndex: number, errorCode: number, error: unknown) => void
+  /** 指定窗口首帧真正渲染完成,适合用于结束窗口 loading。 */
+  firstFrameDisplay?: (windowIndex: number, width: number, height: number) => void
+  /** 当前设备解码性能不足。 */
+  performanceLack?: (windowIndex: number) => void
+  /** 流播放结束。实时预览正常情况下不会主动结束。 */
+  StreamEnd?: (windowIndex: number) => void
+  /** 指定窗口在 interruptTime 秒内没有收到码流。 */
+  InterruptStream?: (windowIndex: number, interruptTime: number) => void
+  /** 用户选中了某个播放窗口。 */
+  windowEventSelect?: (windowIndex: number) => void
+  /** 鼠标进入某个播放窗口。 */
+  windowEventOver?: (windowIndex: number) => void
+  /** 鼠标离开某个播放窗口。 */
+  windowEventOut?: (windowIndex: number) => void
+  /** 鼠标在某个播放窗口中松开。 */
+  windowEventUp?: (windowIndex: number) => void
+  /** 整体全屏状态发生变化,包括按钮和双击触发的变化。 */
+  windowFullCcreenChange?: (fullscreen: boolean) => void
+}
+
+/** 当前页面使用到的 H5player 2.5.1 实例方法。 */
+export interface HikvisionPlayerInstance {
+  /** 注册窗口及播放状态回调。不同构建版本可能返回 Promise 或 void。 */
+  JS_SetWindowControlCallback: (events: HikvisionPlayerEvents) => Promise<void> | void
+  /** 在指定窗口播放实时流;预览场景不传回放开始、结束时间。 */
+  JS_Play: (
+    url: string,
+    options: {
+      playURL: string
+      mode: 0 | 1
+      keepDecoder: 0 | 1
+    },
+    windowIndex: number
+  ) => Promise<void>
+  /** 停止指定窗口;省略下标时 SDK 会使用当前选中窗口。 */
+  JS_Stop: (windowIndex?: number) => Promise<void>
+  /** 停止当前实例中的全部实时预览。 */
+  JS_StopRealPlayAll: () => Promise<void>
+  /** 调整分屏规格,参数 1~4 分别对应 1×1~4×4。 */
+  JS_ArrangeWindow: (split: HikvisionSplitColumns) => Promise<void> | void
+  /** 主动切换 SDK 内部选中的窗口。 */
+  JS_SelectWnd: (windowIndex: number) => Promise<void>
+  /** 切换整个播放器区域的全屏状态。 */
+  JS_FullScreenDisplay: (fullscreen: boolean) => Promise<void>
+  /** 将单个播放窗口切换为全屏。 */
+  JS_FullScreenSingle: (windowIndex: number) => Promise<void>
+  /** 通知 SDK 重新计算播放器及全部子窗口尺寸。 */
+  JS_Resize: (width?: number, height?: number) => Promise<void>
+  /** 释放播放器创建的 DOM、Worker、WASM 和媒体资源。 */
+  JS_Destroy: () => Promise<void> | void
+}
+
+type HikvisionPlayerConstructor = new (options: HikvisionPlayerOptions) => HikvisionPlayerInstance
+
+declare global {
+  interface Window {
+    JSPlugin?: HikvisionPlayerConstructor
+  }
+}
+
+/**
+ * SDK 脚本的共享加载任务。
+ * 多个初始化请求同时到达时复用同一个 Promise,避免重复插入 script 标签。
+ */
+let loadPromise: Promise<HikvisionPlayerConstructor> | undefined
+
+/** 计算部署环境中的 SDK 目录,同时兼容应用部署在非根路径的情况。 */
+function getBasePath() {
+  const appBasePath = import.meta.env.BASE_URL.endsWith('/')
+    ? import.meta.env.BASE_URL
+    : `${import.meta.env.BASE_URL}/`
+  return `${appBasePath}h5player`
+}
+
+export function getHikvisionPlayerBasePath() {
+  return getBasePath()
+}
+
+/**
+ * 按需加载 h5player.min.js 并返回其暴露在 window 上的 JSPlugin 构造器。
+ * 加载失败时会清理 script 和缓存 Promise,使用户后续操作能够重新尝试初始化。
+ */
+export function loadHikvisionPlayer() {
+  if (window.JSPlugin) return Promise.resolve(window.JSPlugin)
+  if (loadPromise) return loadPromise
+
+  loadPromise = new Promise<HikvisionPlayerConstructor>((resolve, reject) => {
+    const script = document.createElement('script')
+    script.src = `${getBasePath()}/h5player.min.js`
+    script.async = true
+    script.dataset.hikvisionH5player = 'true'
+    script.onload = () => {
+      if (window.JSPlugin) {
+        resolve(window.JSPlugin)
+        return
+      }
+      loadPromise = undefined
+      script.remove()
+      reject(new Error('H5player 加载完成,但未找到 JSPlugin'))
+    }
+    script.onerror = () => {
+      loadPromise = undefined
+      script.remove()
+      reject(new Error('H5player 脚本加载失败'))
+    }
+    document.head.appendChild(script)
+  })
+
+  return loadPromise
+}

+ 1211 - 0
src/views/pms/video_center/haikang/index.vue

@@ -0,0 +1,1211 @@
+<script lang="ts" setup>
+import { HikvisionApi, type HikvisionCameraNode, type HikvisionTreeNode } from '@/api/pms/hikvision'
+import grid16Icon from '@/assets/icons/grid-16.svg'
+import gridFourIcon from '@/assets/icons/grid-four.svg'
+import gridNineIcon from '@/assets/icons/grid-nine.svg'
+import gridOneIcon from '@/assets/icons/grid-one.svg'
+import {
+  getHikvisionPlayerBasePath,
+  loadHikvisionPlayer,
+  type HikvisionPlayerInstance,
+  type HikvisionSplitColumns
+} from '@/utils/hikvisionH5Player'
+import { Loading, Location, Search, VideoCamera } from '@element-plus/icons-vue'
+import type { LoadFunction } from 'element-plus/es/components/tree/src/tree.type'
+
+defineOptions({ name: 'HikvisionVideoCenter' })
+
+const treeRef = ref()
+const layoutTriggerRef = ref<HTMLElement>()
+const playerContainerRef = ref<HTMLElement>()
+const keyword = ref('')
+const treeData = ref<HikvisionTreeNode[]>([])
+const expandedKeys = ref<string[]>([])
+const treeLoading = ref(false)
+const playerContainerId = 'hikvision-preview-player'
+const previewLoadingWindowIndexes = ref<number[]>([])
+const previewWindowIndexes = ref<number[]>([])
+const previewWindowCameras = ref<Record<number, HikvisionCameraNode>>({})
+const interruptedWindowIndexes = ref<number[]>([])
+const previewError = ref('')
+const currentSplitCount = ref<SplitCount>(4)
+const activeWindowIndex = ref(0)
+const hoveredWindowIndex = ref<number>()
+const splitPopoverVisible = ref(false)
+const sidebarCollapsed = ref(false)
+const stageFullscreen = ref(false)
+
+type SplitCount = 1 | 4 | 9 | 16
+
+// SDK 的分屏参数是边长:4 表示最大 4×4,2 表示默认 2×2。
+const MAX_SPLIT_COLUMNS = 4
+const DEFAULT_SPLIT_COLUMNS = 2
+
+const splitOptions: Array<{ count: SplitCount; columns: HikvisionSplitColumns; icon: string }> = [
+  { count: 1, columns: 1, icon: gridOneIcon },
+  { count: 4, columns: 2, icon: gridFourIcon },
+  { count: 9, columns: 3, icon: gridNineIcon },
+  { count: 16, columns: 4, icon: grid16Icon }
+]
+
+let player: HikvisionPlayerInstance | undefined
+// 同一时刻只允许一个播放器初始化任务,避免快速操作时创建多个 JSPlugin 实例。
+let playerInitPromise: Promise<HikvisionPlayerInstance> | undefined
+let previewErrorTimer: ReturnType<typeof window.setTimeout> | undefined
+
+/**
+ * 记录每个窗口当前显示 loading 的请求编号。
+ * 关闭 loading 时会核对编号,防止旧请求把新请求的 loading 提前关闭。
+ */
+const previewLoadingWindowRequests = new Map<number, number>()
+
+/** 每个窗口最近一次预览请求的编号,用于让不同窗口并发、同一窗口只保留最新请求。 */
+const previewWindowRequests = new Map<number, number>()
+
+/**
+ * 已经进入 JS_Play 阶段的请求。
+ * 该状态用来区分“旧视频在请求新地址期间触发的回调”和“新视频自身触发的回调”。
+ */
+const previewPlayingWindowRequests = new Map<number, number>()
+let nextPreviewRequestId = 0
+
+// SDK 异步加载完成前页面可能已经卸载,使用该标记阻止卸载后继续创建播放器。
+let componentUnmounted = false
+
+// ResizeObserver 负责容器尺寸变化,requestAnimationFrame 用于合并同一帧内的重复 resize。
+let playerResizeObserver: ResizeObserver | undefined
+let playerResizeFrame: number | undefined
+
+const currentSplitColumns = computed(() => Math.sqrt(currentSplitCount.value))
+const currentSplitRows = computed(() => currentSplitColumns.value)
+const currentSplitWindowIndexes = computed(() =>
+  Array.from({ length: currentSplitCount.value }, (_, index) => index)
+)
+
+const treeProps = {
+  children: 'children',
+  label: 'name',
+  isLeaf: (data: HikvisionTreeNode) => data.nodeType === 'camera'
+}
+
+function clearPreviewErrorTimer() {
+  if (previewErrorTimer === undefined) return
+  window.clearTimeout(previewErrorTimer)
+  previewErrorTimer = undefined
+}
+
+function clearPreviewError() {
+  clearPreviewErrorTimer()
+  previewError.value = ''
+}
+
+function showPreviewError(message: string) {
+  clearPreviewErrorTimer()
+  previewError.value = message
+  previewErrorTimer = setTimeout(() => {
+    previewError.value = ''
+    previewErrorTimer = undefined
+  }, 2000)
+}
+
+/** 更新指定窗口的 loading,并确保旧请求不能覆盖新请求的状态。 */
+function setWindowPreviewLoading(windowIndex: number, loading: boolean, requestId?: number) {
+  if (loading) {
+    if (requestId !== undefined) previewLoadingWindowRequests.set(windowIndex, requestId)
+    if (!previewLoadingWindowIndexes.value.includes(windowIndex)) {
+      previewLoadingWindowIndexes.value = [...previewLoadingWindowIndexes.value, windowIndex]
+    }
+    return
+  }
+
+  if (requestId !== undefined && previewLoadingWindowRequests.get(windowIndex) !== requestId) return
+
+  previewLoadingWindowRequests.delete(windowIndex)
+  previewLoadingWindowIndexes.value = previewLoadingWindowIndexes.value.filter(
+    (loadingWindowIndex) => loadingWindowIndex !== windowIndex
+  )
+}
+
+function clearWindowPreviewLoading() {
+  previewLoadingWindowRequests.clear()
+  previewLoadingWindowIndexes.value = []
+}
+
+/** 为指定窗口创建新请求编号;只会替换该窗口,不影响其他窗口正在进行的预览。 */
+function beginWindowPreviewRequest(windowIndex: number) {
+  const requestId = ++nextPreviewRequestId
+  previewWindowRequests.set(windowIndex, requestId)
+  previewPlayingWindowRequests.delete(windowIndex)
+  setWindowPreviewLoading(windowIndex, true, requestId)
+  return requestId
+}
+
+/** 判断异步结果是否仍属于该窗口的最新一次操作。 */
+function isCurrentWindowPreviewRequest(windowIndex: number, requestId: number) {
+  return previewWindowRequests.get(windowIndex) === requestId
+}
+
+/** 取消指定窗口的待处理请求,同时移除该窗口的 loading。 */
+function cancelWindowPreviewRequest(windowIndex: number) {
+  previewWindowRequests.delete(windowIndex)
+  previewPlayingWindowRequests.delete(windowIndex)
+  setWindowPreviewLoading(windowIndex, false)
+}
+
+/** 停止全部操作时统一使所有尚未返回的异步结果失效。 */
+function cancelAllWindowPreviewRequests() {
+  previewWindowRequests.clear()
+  previewPlayingWindowRequests.clear()
+  clearWindowPreviewLoading()
+}
+
+function setWindowPreviewSelected(windowIndex: number, selected: boolean) {
+  if (selected) {
+    if (!previewWindowIndexes.value.includes(windowIndex)) {
+      previewWindowIndexes.value = [...previewWindowIndexes.value, windowIndex]
+    }
+    return
+  }
+
+  previewWindowIndexes.value = previewWindowIndexes.value.filter(
+    (previewWindowIndex) => previewWindowIndex !== windowIndex
+  )
+}
+
+function setWindowPreviewCamera(windowIndex: number, camera?: HikvisionCameraNode) {
+  const nextWindowCameras = { ...previewWindowCameras.value }
+  if (camera) {
+    nextWindowCameras[windowIndex] = camera
+  } else {
+    delete nextWindowCameras[windowIndex]
+  }
+  previewWindowCameras.value = nextWindowCameras
+}
+
+function setWindowInterrupted(windowIndex: number, interrupted: boolean) {
+  if (interrupted) {
+    if (!interruptedWindowIndexes.value.includes(windowIndex)) {
+      interruptedWindowIndexes.value = [...interruptedWindowIndexes.value, windowIndex]
+    }
+    return
+  }
+
+  interruptedWindowIndexes.value = interruptedWindowIndexes.value.filter(
+    (interruptedWindowIndex) => interruptedWindowIndex !== windowIndex
+  )
+}
+
+/** 清空一个窗口在前端维护的请求、监控点、播放和断流状态。 */
+function clearWindowPreview(windowIndex: number) {
+  cancelWindowPreviewRequest(windowIndex)
+  setWindowPreviewSelected(windowIndex, false)
+  setWindowPreviewCamera(windowIndex)
+  setWindowInterrupted(windowIndex, false)
+}
+
+function clearWindowPreviewSelected() {
+  previewWindowIndexes.value = []
+  previewWindowCameras.value = {}
+  interruptedWindowIndexes.value = []
+}
+
+function getWindowPreviewStatus(windowIndex: number) {
+  if (previewLoadingWindowIndexes.value.includes(windowIndex)) return '正在加载'
+  if (interruptedWindowIndexes.value.includes(windowIndex)) return '画面中断'
+  if (previewWindowIndexes.value.includes(windowIndex)) return '正在预览'
+  return '未预览'
+}
+
+function getWindowPreviewTitle(windowIndex: number) {
+  return previewWindowCameras.value[windowIndex]?.name || '未选择监控'
+}
+
+async function loadRootTree() {
+  treeLoading.value = true
+  try {
+    treeData.value = await HikvisionApi.getTreeNodes('-1')
+    const root = treeData.value[0]
+    expandedKeys.value = root?.nodeType === 'region' ? [root.indexCode] : []
+  } catch {
+    treeData.value = []
+  } finally {
+    treeLoading.value = false
+  }
+}
+
+const loadTreeNode: LoadFunction = async (node, resolve, reject) => {
+  if (node.level === 0) {
+    resolve([])
+    return
+  }
+
+  const data = node.data as HikvisionTreeNode
+  if (data.nodeType !== 'region') {
+    resolve([])
+    return
+  }
+
+  if (data.children.length) {
+    resolve(data.children)
+    return
+  }
+
+  try {
+    resolve(await HikvisionApi.getTreeNodes(data.indexCode))
+  } catch {
+    reject()
+  }
+}
+
+function filterTreeNode(value: string, data: HikvisionTreeNode) {
+  if (!value.trim()) return true
+  return data.name.toLowerCase().includes(value.trim().toLowerCase())
+}
+
+function initPlayer(): Promise<HikvisionPlayerInstance> {
+  if (player) return Promise.resolve(player)
+  if (playerInitPromise) return playerInitPromise
+
+  clearPreviewError()
+  playerInitPromise = (async () => {
+    // 初始化未完成前不写入全局 player,便于页面卸载或注册回调失败时完整回收半成品实例。
+    let initializingPlayer: HikvisionPlayerInstance | undefined
+    try {
+      await nextTick()
+      const JSPlugin = await loadHikvisionPlayer()
+      if (componentUnmounted) throw new Error('页面已卸载,取消初始化播放器')
+
+      initializingPlayer = new JSPlugin({
+        szId: playerContainerId,
+        szBasePath: getHikvisionPlayerBasePath(),
+        iMaxSplit: MAX_SPLIT_COLUMNS,
+        iCurrentSplit: DEFAULT_SPLIT_COLUMNS,
+        mseWorkerEnable: false,
+        bSupporDoubleClickFull: true,
+        oStyle: { borderSelect: '#facc15' }
+      })
+
+      // 开发指南要求创建实例后先注册事件,再调用 JS_Play 等播放接口。
+      await Promise.resolve(
+        initializingPlayer.JS_SetWindowControlCallback({
+          windowEventSelect: (windowIndex) => {
+            activeWindowIndex.value = windowIndex
+          },
+          windowEventOver: (windowIndex) => {
+            // 使用 SDK 提供的窗口进入事件控制悬浮工具栏,无需自行计算鼠标坐标。
+            hoveredWindowIndex.value = windowIndex
+          },
+          windowEventOut: (windowIndex) => {
+            if (hoveredWindowIndex.value === windowIndex) hoveredWindowIndex.value = undefined
+          },
+          windowFullCcreenChange: (fullscreen) => {
+            // 双击也能触发全屏,因此以 SDK 回调作为页面全屏状态的最终来源。
+            stageFullscreen.value = fullscreen
+            schedulePlayerResize()
+          },
+          firstFrameDisplay: (windowIndex) => {
+            // JS_Play 成功只代表播放命令已接受;首帧回调才代表用户真正看到了画面。
+            const playingRequestId = previewPlayingWindowRequests.get(windowIndex)
+            if (
+              playingRequestId === undefined ||
+              !isCurrentWindowPreviewRequest(windowIndex, playingRequestId)
+            ) {
+              return
+            }
+            setWindowInterrupted(windowIndex, false)
+            setWindowPreviewSelected(windowIndex, true)
+            cancelWindowPreviewRequest(windowIndex)
+          },
+          InterruptStream: (windowIndex, interruptTime) => {
+            // 获取新地址期间收到的回调可能属于旧流;新流尚未进入 JS_Play 时忽略该旧回调。
+            if (
+              previewWindowRequests.has(windowIndex) &&
+              !previewPlayingWindowRequests.has(windowIndex)
+            ) {
+              return
+            }
+            if (!previewWindowCameras.value[windowIndex]) return
+            cancelWindowPreviewRequest(windowIndex)
+            setWindowInterrupted(windowIndex, true)
+            showPreviewError(`监控画面已中断(${interruptTime} 秒未收到码流)`)
+          },
+          StreamEnd: (windowIndex) => {
+            // 与断流回调相同,避免旧流结束事件清掉正在请求的新监控点。
+            if (
+              previewWindowRequests.has(windowIndex) &&
+              !previewPlayingWindowRequests.has(windowIndex)
+            ) {
+              return
+            }
+            if (!previewWindowCameras.value[windowIndex]) return
+            clearWindowPreview(windowIndex)
+            showPreviewError('监控画面已结束')
+          },
+          performanceLack: () => {
+            // SDK 已经完成性能判断,页面只负责给出短暂提示,不自行推断设备性能。
+            showPreviewError('当前设备性能不足,监控画面可能卡顿')
+          },
+          pluginErrorHandler: (windowIndex, errorCode, error) => {
+            console.error('H5player 播放器错误', errorCode, error)
+            // 新地址仍在请求时,窗口里的 SDK 错误属于即将替换的旧流,不应取消新请求。
+            if (
+              windowIndex >= 0 &&
+              previewWindowRequests.has(windowIndex) &&
+              !previewPlayingWindowRequests.has(windowIndex)
+            ) {
+              return
+            }
+            if (windowIndex >= 0) clearWindowPreview(windowIndex)
+            showPreviewError(`播放器异常(${errorCode})`)
+          }
+        })
+      )
+      if (componentUnmounted) throw new Error('页面已卸载,取消初始化播放器')
+
+      // 回调注册成功且页面仍存在后,播放器实例才正式对其他操作可见。
+      player = initializingPlayer
+      initializingPlayer = undefined
+      schedulePlayerResize()
+      return player
+    } catch (error) {
+      // 初始化失败也要销毁半成品实例,否则 Worker、WASM 或媒体资源可能残留。
+      await initializingPlayer?.JS_StopRealPlayAll().catch(() => undefined)
+      await Promise.resolve(initializingPlayer?.JS_Destroy()).catch(() => undefined)
+      console.error('H5player 初始化失败', error)
+      if (!componentUnmounted) {
+        showPreviewError(error instanceof Error ? error.message : '播放器初始化失败')
+      }
+      throw error
+    } finally {
+      playerInitPromise = undefined
+    }
+  })()
+
+  return playerInitPromise
+}
+
+async function playCameraPreview(camera: HikvisionCameraNode) {
+  // 点击监控点时固定目标窗口,之后用户切换选中窗口不会改变本次请求的播放位置。
+  const windowIndex = activeWindowIndex.value
+  const requestId = beginWindowPreviewRequest(windowIndex)
+  clearPreviewError()
+
+  try {
+    // SDK 初始化和后端预览地址互不依赖,可以并行等待以缩短首次点播耗时。
+    const [currentPlayer, previewUrl] = await Promise.all([
+      initPlayer(),
+      HikvisionApi.getPreviewUrl(camera.indexCode)
+    ])
+    if (!isCurrentWindowPreviewRequest(windowIndex, requestId)) return
+    if (!previewUrl) throw new Error('预览地址为空')
+
+    // keepDecoder 为 0 时切换监控点前先停止旧流,确保旧解码资源得到回收。
+    await currentPlayer.JS_Stop(windowIndex).catch(() => undefined)
+    if (!isCurrentWindowPreviewRequest(windowIndex, requestId)) return
+    setWindowPreviewSelected(windowIndex, false)
+    setWindowInterrupted(windowIndex, false)
+    setWindowPreviewCamera(windowIndex, camera)
+
+    // 在调用 JS_Play 前记录阶段,使首帧、断流和错误回调能够识别当前新流。
+    previewPlayingWindowRequests.set(windowIndex, requestId)
+    await currentPlayer.JS_Play(
+      previewUrl,
+      {
+        playURL: previewUrl,
+        mode: 0,
+        keepDecoder: 0
+      },
+      windowIndex
+    )
+    // 这里不关闭 loading;只有 firstFrameDisplay、错误或断流回调才能结束窗口 loading。
+  } catch (error) {
+    if (!isCurrentWindowPreviewRequest(windowIndex, requestId)) return
+    clearWindowPreview(windowIndex)
+    console.error('监控点预览失败', error)
+    showPreviewError(error instanceof Error ? error.message : '获取预览地址或播放失败')
+  }
+}
+
+async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColumns) {
+  splitPopoverVisible.value = false
+  if (currentSplitCount.value === count) return
+
+  try {
+    const currentPlayer = await initPlayer()
+
+    // 从大分屏切换到小分屏时,先计算将被隐藏的窗口,例如 9 分屏切到 4 分屏会清理 4~8。
+    const hiddenWindowIndexes = Array.from(
+      { length: Math.max(currentSplitCount.value - count, 0) },
+      (_, index) => count + index
+    )
+    await Promise.all(
+      hiddenWindowIndexes.map((windowIndex) =>
+        currentPlayer.JS_Stop(windowIndex).catch(() => undefined)
+      )
+    )
+    // SDK 流和页面状态必须同时清理,避免隐藏窗口继续占用连接及解码资源。
+    hiddenWindowIndexes.forEach(clearWindowPreview)
+    await currentPlayer.JS_ArrangeWindow(columns)
+    currentSplitCount.value = count
+    activeWindowIndex.value = 0
+
+    // 分屏后显式同步 SDK 的选中窗口,保证黄色边框和页面 activeWindowIndex 一致。
+    await currentPlayer.JS_SelectWnd(0).catch(() => undefined)
+    await nextTick()
+    schedulePlayerResize()
+  } catch (error) {
+    console.error('切换监控分屏失败', error)
+    showPreviewError(error instanceof Error ? error.message : '切换分屏失败')
+  }
+}
+
+function handleNodeClick(data: HikvisionTreeNode) {
+  if (data.nodeType !== 'camera') return
+
+  // 已知离线或不可用的监控点直接提示,不请求后端预览地址,也不影响当前正在播放的画面。
+  if (data.available === false || data.onlineStatus === false) {
+    showPreviewError(`${data.name} 不在线,无法预览`)
+    return
+  }
+  void playCameraPreview(data)
+}
+
+function resizePlayer() {
+  playerResizeFrame = undefined
+  const playerContainer = playerContainerRef.value
+  if (!playerContainer) {
+    player?.JS_Resize().catch(() => undefined)
+    return
+  }
+
+  const { clientWidth, clientHeight } = playerContainer
+  if (!clientWidth || !clientHeight) return
+  player?.JS_Resize(clientWidth, clientHeight).catch(() => undefined)
+}
+
+/** 合并同一帧内的尺寸变化,避免侧栏动画或布局变化时连续调用昂贵的 SDK resize。 */
+function schedulePlayerResize() {
+  if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
+  playerResizeFrame = requestAnimationFrame(resizePlayer)
+}
+
+async function stopAllPreviews() {
+  // 先让所有未完成的接口请求失效,防止停止完成后某个旧请求又调用 JS_Play。
+  cancelAllWindowPreviewRequests()
+  clearPreviewError()
+
+  try {
+    await player?.JS_StopRealPlayAll()
+    clearWindowPreviewSelected()
+    activeWindowIndex.value = 0
+  } catch (error) {
+    console.error('关闭全部监控画面失败', error)
+    showPreviewError(error instanceof Error ? error.message : '关闭全部监控画面失败')
+  }
+}
+
+async function toggleFullscreen() {
+  try {
+    const currentPlayer = await initPlayer()
+    await currentPlayer.JS_FullScreenDisplay(!stageFullscreen.value)
+  } catch (error) {
+    console.error('切换监控全屏失败', error)
+    showPreviewError(error instanceof Error ? error.message : '切换全屏失败')
+  }
+}
+
+async function toggleSidebar() {
+  sidebarCollapsed.value = !sidebarCollapsed.value
+  await nextTick()
+  schedulePlayerResize()
+}
+
+async function destroyPlayer() {
+  // 卸载后不再接收异步请求结果、错误定时器或任何尺寸变化通知。
+  cancelAllWindowPreviewRequests()
+  clearPreviewErrorTimer()
+  window.removeEventListener('resize', schedulePlayerResize)
+  playerResizeObserver?.disconnect()
+  playerResizeObserver = undefined
+  if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
+  playerResizeFrame = undefined
+  if (!player) return
+
+  const currentPlayer = player
+  player = undefined
+
+  // 多分屏场景必须停止全部实时流;只停止窗口 0 会遗留其他窗口连接。
+  await currentPlayer.JS_StopRealPlayAll().catch(() => undefined)
+  await Promise.resolve(currentPlayer.JS_Destroy()).catch(() => undefined)
+}
+
+watch(keyword, (value) => treeRef.value?.filter(value))
+
+onMounted(() => {
+  componentUnmounted = false
+  window.addEventListener('resize', schedulePlayerResize)
+  if (playerContainerRef.value) {
+    // window.resize 无法覆盖侧栏折叠等局部布局变化,因此额外观察播放器容器本身。
+    playerResizeObserver = new ResizeObserver(schedulePlayerResize)
+    playerResizeObserver.observe(playerContainerRef.value)
+  }
+  void loadRootTree()
+  void initPlayer().catch(() => undefined)
+})
+
+onBeforeUnmount(() => {
+  componentUnmounted = true
+  void destroyPlayer()
+})
+</script>
+<template>
+  <div
+    class="hikvision-shell h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <aside class="monitor-panel" :class="{ 'is-collapsed': sidebarCollapsed }">
+      <div class="panel-title">
+        <div>
+          <div class="eyebrow">MONITOR DIRECTORY</div>
+          <h2>监控点目录</h2>
+        </div>
+      </div>
+
+      <el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索已加载监控点" />
+
+      <div v-loading="treeLoading" class="monitor-tree-wrap">
+        <el-tree
+          ref="treeRef"
+          class="monitor-tree"
+          :data="treeData"
+          :props="treeProps"
+          :load="loadTreeNode"
+          :default-expanded-keys="expandedKeys"
+          node-key="indexCode"
+          lazy
+          highlight-current
+          :expand-on-click-node="false"
+          :filter-node-method="filterTreeNode"
+          empty-text="暂无监控点"
+          @node-click="handleNodeClick">
+          <template #default="{ data }">
+            <div
+              class="tree-node"
+              :class="{
+                'is-camera': data.nodeType === 'camera',
+                'is-unavailable': data.available === false
+              }">
+              <el-icon class="node-icon">
+                <Location v-if="data.nodeType === 'region'" />
+                <VideoCamera v-else />
+              </el-icon>
+              <span class="node-name" :title="data.name">{{ data.name }}</span>
+              <span
+                v-if="data.nodeType === 'camera'"
+                class="status-dot"
+                :class="{
+                  online: data.onlineStatus === true,
+                  offline: data.onlineStatus === false
+                }"
+                :title="
+                  data.onlineStatus === true
+                    ? '在线'
+                    : data.onlineStatus === false
+                      ? '离线'
+                      : '状态未知'
+                "></span>
+            </div>
+          </template>
+        </el-tree>
+      </div>
+
+      <div class="panel-tip">搜索仅筛选当前已加载的节点</div>
+    </aside>
+
+    <main class="video-workspace">
+      <section class="video-stage">
+        <div :id="playerContainerId" ref="playerContainerRef" class="player-container"></div>
+        <div
+          class="window-loading-layer"
+          :style="{
+            gridTemplateColumns: `repeat(${currentSplitColumns}, minmax(0, 1fr))`,
+            gridTemplateRows: `repeat(${currentSplitRows}, minmax(0, 1fr))`
+          }">
+          <div
+            v-for="windowIndex in currentSplitWindowIndexes"
+            :key="windowIndex"
+            class="window-loading-cell"
+            :class="{
+              'is-active': activeWindowIndex === windowIndex,
+              'is-hovered': hoveredWindowIndex === windowIndex
+            }">
+            <div class="window-hover-title">
+              <span class="window-title-main">
+                <i class="i-lucide:video"></i>
+                <span>{{ getWindowPreviewTitle(windowIndex) }}</span>
+              </span>
+              <span class="window-title-status">
+                {{ getWindowPreviewStatus(windowIndex) }}
+                <i class="i-lucide:circle-x"></i>
+              </span>
+            </div>
+            <div
+              v-if="previewLoadingWindowIndexes.includes(windowIndex)"
+              class="window-loading-indicator">
+              <el-icon class="is-loading"><Loading /></el-icon>
+            </div>
+            <div
+              v-else-if="!previewWindowIndexes.includes(windowIndex)"
+              class="window-empty-indicator">
+              <el-icon><VideoCamera /></el-icon>
+              <span>暂无监控</span>
+            </div>
+            <div class="window-hover-actions" aria-hidden="true">
+              <button type="button" tabindex="-1">
+                <i class="i-lucide:volume-2"></i>
+              </button>
+              <button type="button" tabindex="-1">
+                <i class="i-lucide:camera"></i>
+              </button>
+              <button type="button" tabindex="-1">
+                <i class="i-lucide:zoom-in"></i>
+              </button>
+              <button type="button" tabindex="-1">
+                <i class="i-lucide:user-round"></i>
+              </button>
+              <button type="button" tabindex="-1">
+                <i class="i-lucide:settings"></i>
+              </button>
+            </div>
+          </div>
+        </div>
+
+        <div v-if="previewError" class="stage-placeholder is-error">
+          <el-icon><VideoCamera /></el-icon>
+          <p>{{ previewError }}</p>
+        </div>
+        <div class="video-toolbar">
+          <el-tooltip
+            :content="sidebarCollapsed ? '展开监控点目录' : '收起监控点目录'"
+            placement="top">
+            <button
+              type="button"
+              class="sidebar-toggle"
+              :aria-label="sidebarCollapsed ? '展开监控点目录' : '收起监控点目录'"
+              :aria-expanded="!sidebarCollapsed"
+              @click="toggleSidebar">
+              <i v-if="sidebarCollapsed" class="i-lucide:panel-left-open"></i>
+              <i v-else class="i-lucide:panel-left-close"></i>
+            </button>
+          </el-tooltip>
+
+          <div class="toolbar-actions">
+            <el-tooltip content="关闭全部监控画面" placement="top">
+              <button
+                type="button"
+                class="stop-all-trigger"
+                aria-label="关闭全部监控画面"
+                @click="stopAllPreviews">
+                <i class="i-lucide:video-off"></i>
+              </button>
+            </el-tooltip>
+
+            <el-tooltip content="分屏布局" placement="top" :disabled="splitPopoverVisible">
+              <button
+                ref="layoutTriggerRef"
+                type="button"
+                class="layout-trigger"
+                :class="{ active: splitPopoverVisible }"
+                aria-label="选择分屏布局"
+                :aria-expanded="splitPopoverVisible">
+                <i class="i-lucide:layout-grid"></i>
+              </button>
+            </el-tooltip>
+
+            <el-popover
+              v-model:visible="splitPopoverVisible"
+              :virtual-ref="layoutTriggerRef"
+              placement="top-end"
+              trigger="click"
+              :width="176"
+              :show-arrow="false"
+              virtual-triggering
+              popper-class="hikvision-split-popover">
+              <div class="split-menu" role="group" aria-label="分屏布局">
+                <button
+                  v-for="option in splitOptions"
+                  :key="option.count"
+                  type="button"
+                  class="split-menu-button"
+                  :class="{ active: currentSplitCount === option.count }"
+                  :title="`${option.count} 分屏`"
+                  :aria-label="`切换为 ${option.count} 分屏`"
+                  :aria-pressed="currentSplitCount === option.count"
+                  @click="handleSplitChange(option.count, option.columns)">
+                  <span
+                    class="split-menu-icon"
+                    :style="{ '--split-icon': `url(${option.icon})` }"
+                    aria-hidden="true"></span>
+                </button>
+              </div>
+            </el-popover>
+
+            <el-tooltip :content="stageFullscreen ? '退出全屏' : '全屏'" placement="top">
+              <button
+                type="button"
+                class="fullscreen-trigger"
+                :aria-label="stageFullscreen ? '退出全屏' : '全屏'"
+                :aria-pressed="stageFullscreen"
+                @click="toggleFullscreen">
+                <i v-if="stageFullscreen" class="i-lucide:minimize"></i>
+                <i v-else class="i-lucide:maximize"></i>
+              </button>
+            </el-tooltip>
+          </div>
+        </div>
+      </section>
+    </main>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.hikvision-shell {
+  --navy: #0f2742;
+  --video-toolbar-height: 49px;
+
+  display: flex;
+  width: 100%;
+  overflow: hidden;
+  color: #1e293b;
+  background: #e8edf3;
+  border: 1px solid #cbd5e1;
+  border-radius: 8px;
+}
+
+.monitor-panel {
+  --monitor-panel-width: 280px;
+
+  display: flex;
+  width: var(--monitor-panel-width);
+  padding: 18px 14px;
+  overflow: hidden;
+  background: #f8fafc;
+  border-right: 1px solid #cbd5e1;
+  box-sizing: border-box;
+  flex: 0 0 var(--monitor-panel-width);
+  flex-direction: column;
+  gap: 14px;
+}
+
+.monitor-panel.is-collapsed {
+  width: 0;
+  flex-basis: 0;
+  padding-right: 0;
+  padding-left: 0;
+  border-right: 0;
+}
+
+.panel-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.panel-title h2 {
+  margin: 3px 0 0;
+  font-size: 20px;
+  color: var(--navy);
+}
+
+.eyebrow {
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 1.5px;
+  color: #64748b;
+}
+
+.monitor-tree-wrap {
+  min-height: 0;
+  flex: 1;
+  margin-right: -14px;
+  overflow: auto;
+}
+
+.monitor-tree {
+  min-width: 100%;
+  padding-right: 26px;
+  background: transparent;
+  box-sizing: border-box;
+
+  :deep(.el-tree-node__content) {
+    height: 38px;
+    margin-bottom: 3px;
+    border-radius: 6px;
+  }
+
+  :deep(.el-tree-node__content:hover) {
+    background: #e9eff6;
+  }
+
+  :deep(.el-tree-node.is-current > .el-tree-node__content) {
+    color: #0f3d69;
+    background: #dce9f6;
+    box-shadow: inset 3px 0 #2563eb;
+  }
+}
+
+.tree-node {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  align-items: center;
+  gap: 7px;
+  padding-right: 8px;
+  color: #334155;
+}
+
+.tree-node.is-camera .node-icon {
+  color: #2563eb;
+}
+
+.tree-node.is-unavailable {
+  color: #94a3b8;
+}
+
+.node-icon {
+  flex: none;
+  color: #d97706;
+}
+
+.node-name {
+  min-width: 0;
+  flex: 1;
+  overflow: hidden;
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.status-dot {
+  width: 7px;
+  height: 7px;
+  flex: none;
+  background: #94a3b8;
+  border-radius: 50%;
+}
+
+.status-dot.online {
+  background: #22c55e;
+  box-shadow: 0 0 0 3px rgb(34 197 94 / 12%);
+}
+
+.status-dot.offline {
+  background: #ef4444;
+  box-shadow: 0 0 0 3px rgb(239 68 68 / 10%);
+}
+
+.panel-tip {
+  font-size: 11px;
+  color: #94a3b8;
+  text-align: center;
+}
+
+.video-workspace {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  overflow: hidden;
+  background: #0b1118;
+}
+
+.video-stage {
+  position: relative;
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  flex: 1;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: #0b1118;
+}
+
+.player-container {
+  position: absolute;
+  width: 100%;
+  height: calc(100% - var(--video-toolbar-height));
+  overflow: hidden;
+  background: #0b1118;
+  box-sizing: border-box;
+  inset: 0 0 var(--video-toolbar-height);
+}
+
+.player-container :deep(.parent-wnd) {
+  width: 100% !important;
+  height: 100% !important;
+  overflow: hidden;
+}
+
+.player-container :deep(.sub-wnd) {
+  box-sizing: border-box !important;
+}
+
+.window-loading-layer {
+  position: absolute;
+  z-index: 2;
+  display: grid;
+  pointer-events: none;
+  inset: 0 0 var(--video-toolbar-height);
+}
+
+.window-loading-cell {
+  position: relative;
+  min-width: 0;
+  min-height: 0;
+}
+
+.window-loading-cell.is-active::after {
+  position: absolute;
+  z-index: 4;
+  pointer-events: none;
+  border: 2px solid #facc15;
+  content: '';
+  box-shadow:
+    inset 0 0 0 1px rgb(250 204 21 / 62%),
+    0 0 12px rgb(250 204 21 / 32%);
+  inset: 0;
+}
+
+.window-hover-title,
+.window-hover-actions {
+  position: absolute;
+  right: 0;
+  left: 0;
+  z-index: 5;
+  display: flex;
+  height: 30px;
+  padding: 0 10px;
+  color: #f8fafc;
+  pointer-events: none;
+  opacity: 0;
+  box-sizing: border-box;
+  align-items: center;
+  transition: opacity 0.16s ease;
+}
+
+.window-loading-cell.is-hovered .window-hover-title,
+.window-loading-cell.is-hovered .window-hover-actions {
+  opacity: 1;
+}
+
+.window-hover-title {
+  top: 0;
+  justify-content: space-between;
+  background: linear-gradient(180deg, rgb(15 23 42 / 86%), rgb(15 23 42 / 12%));
+}
+
+.window-title-main,
+.window-title-status {
+  display: inline-flex;
+  min-width: 0;
+  align-items: center;
+  gap: 6px;
+}
+
+.window-title-main {
+  flex: 1;
+  font-size: 13px;
+  font-weight: 700;
+}
+
+.window-title-main span {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.window-title-status {
+  flex: none;
+  margin-left: 10px;
+  font-size: 12px;
+}
+
+.window-hover-actions {
+  bottom: 0;
+  justify-content: flex-end;
+  background: linear-gradient(0deg, rgb(15 23 42 / 88%), rgb(15 23 42 / 10%));
+}
+
+.window-hover-actions button {
+  display: inline-flex;
+  width: 24px;
+  height: 24px;
+  padding: 0;
+  font-size: 15px;
+  color: #e2e8f0;
+  background: transparent;
+  border: 0;
+  align-items: center;
+  justify-content: center;
+}
+
+.window-loading-indicator {
+  position: absolute;
+  z-index: 2;
+  inset: 0;
+  display: flex;
+  font-size: 24px;
+  color: #60a5fa;
+  background: rgb(2 6 23 / 36%);
+  align-items: center;
+  justify-content: center;
+}
+
+.window-empty-indicator {
+  position: absolute;
+  z-index: 1;
+  inset: 0;
+  display: flex;
+  gap: 8px;
+  font-size: 12px;
+  color: rgb(148 163 184 / 82%);
+  background: rgb(2 6 23 / 18%);
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+}
+
+.window-empty-indicator .el-icon {
+  font-size: 22px;
+}
+
+.stage-placeholder {
+  position: relative;
+  z-index: 3;
+  color: #64748b;
+  text-align: center;
+}
+
+.stage-placeholder.is-error {
+  padding: 24px;
+  background: rgb(255 255 255 / 92%);
+  border-radius: 8px;
+}
+
+.stage-placeholder.is-error .el-icon {
+  color: #ef4444;
+}
+
+.stage-placeholder .el-icon {
+  font-size: 54px;
+  color: #cbd5e1;
+}
+
+.stage-placeholder p {
+  margin: 12px 0 5px;
+  font-size: 14px;
+  font-weight: 700;
+  color: #334155;
+}
+
+.video-toolbar {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 4;
+  display: flex;
+  height: 49px;
+  padding: 0 14px;
+  color: #cbd5e1;
+  background: #18212c;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.toolbar-actions {
+  display: inline-flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.sidebar-toggle,
+.stop-all-trigger,
+.fullscreen-trigger,
+.layout-trigger {
+  display: inline-flex;
+  width: 34px;
+  height: 34px;
+  padding: 0;
+  font-size: 22px;
+  color: #cbd5e1;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  border-radius: 4px;
+  align-items: center;
+  justify-content: center;
+}
+
+.sidebar-toggle:hover,
+.stop-all-trigger:hover,
+.fullscreen-trigger:hover,
+.fullscreen-trigger[aria-pressed='true'],
+.layout-trigger:hover,
+.layout-trigger.active {
+  color: #fff;
+  background: #2b3746;
+}
+
+.split-menu {
+  display: flex;
+  gap: 6px;
+  align-items: center;
+  justify-content: center;
+}
+
+.split-menu-button {
+  display: inline-flex;
+  width: 34px;
+  height: 34px;
+  padding: 0;
+  color: #cbd5e1;
+  cursor: pointer;
+  background: transparent;
+  border: 1px solid transparent;
+  border-radius: 3px;
+  align-items: center;
+  justify-content: center;
+}
+
+.split-menu-button:hover {
+  color: #fff;
+  background: #4a515b;
+}
+
+.split-menu-button.active {
+  color: #f43f5e;
+  background: rgb(244 63 94 / 12%);
+  border-color: rgb(244 63 94 / 52%);
+}
+
+.split-menu-icon {
+  width: 24px;
+  height: 24px;
+  background: currentcolor;
+  mask: var(--split-icon) center / contain no-repeat;
+}
+
+:global(.hikvision-split-popover.el-popper) {
+  min-width: 0;
+  padding: 7px;
+  background: #3b4149;
+  border-color: #545c66;
+  box-shadow: 0 8px 20px rgb(15 23 42 / 32%);
+}
+
+@media (width <= 1200px) {
+  .monitor-panel {
+    --monitor-panel-width: 240px;
+  }
+}
+</style>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů