Uni-app(h5) 海康安防视频监控对接:从 WebSocket 到 HLS 的优雅降级方案

66 阅读10分钟

项目背景:本文基于个人项目中的视频监控需求,详细阐述 uni-app 框架下海康威视设备的接入方案,重点介绍双协议支持、自动降级、资源管理等核心技术问题的解决思路。


第一次对接监控系统,写了自己的一些体会,自己遇到的一些问题及解决方案,也算完成第一次创作的任务,有问题的部分,希望jym提出宝贵意见,我应该不改,嘻嘻😁

一、技术选型与架构设计

1.1 协议对比分析

协议类型适用场景延迟表现兼容性实现复杂度
WebSocket (私有协议)H5 平台,支持 JSPlugin低 (1-3s)依赖浏览器环境
HLS跨平台兼容中 (3-10s)优秀
RTSP/RTMP原生 App受限

选型策略:以 WebSocket 为主协议,HLS 作为降级方案,确保在各种异常情况下仍能提供视频服务。

1.2 核心技术栈

// 前端框架
- Vue 3 (Composition API)
- TypeScript
- uni-app (跨平台框架)

// 视频播放
- JSPlugin.js (海康官方 H5 SDK)
- hls.js (WebHLS 播放器)

// 状态管理
- Pinia (带持久化插件)
- uni-app 原生生命周期

1.3 工具准备

1.3.1 海康官方插件包

官方插件包 这里提供了demo 可以测试相关接口以及取流地址是否可用,开发包中主要需要这些文件,放到项目的static(自定义即可)目录下即可 所需文件 主要所用接口: 在这里插入图片描述 这里测试接口的时候按照海康官方文档一步一步执行,文档写的很清晰,最后获取到地址即可。

1.3.2 hls协议实现所需工具

npm下载 video.js,hls.js,实现播放协议的工具有很多,我只使用了这两个,因为hls协议较为简单,所以重点以websocket为主,相关代码也会列出来 海康平台默认hls取得协议是http协议的, 在这里插入图片描述 只要将potocol的参数改为hlss(wss同理),即可获取https协议地址,这个地址主要是为项目需要上政府企业或者安全要求高的企业使用,一般情况下http即可。

二、核心实现流程

2.1 完整业务流程

sequenceDiagram
    participant User as 用户
    participant Page as 监控详情页
    participant API as 后端服务
    participant Player as 播放器

    User->>Page: 点击监控设备
    Page->>Page: 显示 Loading
    Page->>API: 获取设备详情
    API-->>Page: 返回设备信息

    Page->>API: 请求 WS 流地址
    API-->>Page: 返回 WS URL

    Page->>Page: 动态加载 JSPlugin
    alt 加载成功
        Page->>Player: 初始化播放器
        Page->>Player: 开始播放
        Page->>Page: 隐藏 Loading
    else 加载失败
        Page->>API: 请求 HLS 流地址
        API-->>Page: 返回 HLS URL
        Page->>Player: 初始化 HLS 播放器
        Page->>Page: 隐藏 Loading
    end

2.2 API 接口设计

2.2.1 获取视频流地址

这里获取视频流地址,主要是i后端对海康平台做了一层接口转发。实际上参数跟海康开发平台文档一样,主要是获取


export const getStreamUrl = (data: GetPreviewURLsServiceReq) =>
  post('/manage', {
    data: {
      api: 'getPreviewURLsService',
      v: '1.0',
      data,
    }
  });

// 请求参数类型
interface GetPreviewURLsServiceReq {
  /** 监控索引编码 */
  cameraIndexCode: string;
  /** 协议类型 */
  protocol: 'hik' | 'rtsp' | 'rtmp' | 'hls' | 'ws';
  /** 码流类型: 0-主码流, 1-子码流, 2-第三码流 */
  streamType?: number;
  /** 传输协议: 0-UDP, 1-TCP */
  transmode?: number;
}

// 响应数据结构
{
  code: 200,
  msg: 'success',
  data: {
    url: 'ws://stream.example.com/live/xxx',
    expireTime: 3600,
    protocol: 'ws'
  }
}

三、WebSocket 协议实现(主方案)

3.1 动态脚本加载

核心问题:JSPlugin 体积较大(16M),且仅在 H5 环境需要,按需加载。 注意:如果有上政务环境的要求,要先去官网问一下有无文档体积限制,(例如浙江这边,浙政钉要求源码上传,最多不能20M)

解决方案

// src/pages/monitor/detail/index.vue:155-176
const loadJSPluginScript = (): Promise<void> => {
  return new Promise((resolve, reject) => {
    // 1. 检查缓存,避免重复加载
    if (window.JSPlugin) {
      resolve();
      return;
    }

    // 2. 动态创建 script 标签
    const script = document.createElement('script');
    script.type = 'text/javascript';
    // 远程加载(要先在浏览器测试是否输入后能否直接访问)
    script.src = 'http://xxxxx.com/h5Player/h5player.min.js';
    // 本地加载 (这里注意本地加载的话是挂在全局的html文件下)
     script.src = '/static/h5Player/js/JsPlugin.js';

    // 3. 加载成功
    script.onload = () => {
      console.log('JSPlugin 脚本加载成功');
      resolve();
    };

    // 4. 加载失败处理
    script.onerror = (error) => {
      console.error('JSPlugin 脚本加载失败:', error);
      reject(new Error('JSPlugin 加载超时或网络错误'));
    };

    // 5. 添加到文档
    document.head.appendChild(script);
  });
};

关键点

  • ✅ Promise 封装,支持异步等待
  • ✅ 缓存检查,避免重复加载
  • ✅ 明确的错误处理,便于降级
  • ✅ 异步加载,不阻塞页面渲染

3.2 播放器初始化

// src/pages/monitor/detail/index.vue:198-261
const initPlayer = async () => {
  try {
    // 1. 动态加载 JSPlugin
    await loadJSPluginScript();
  } catch (error) {
    console.error('加载JSPlugin失败:', error);
    // 触发降级:记录日志,提示用户
    uni.showToast({
      title: 'WS协议播放器加载失败',
      icon: 'none'
    });
    // 抛出错误,由上层处理降级逻辑
    throw error;
  }

  try {
    // 2. 检查 JSPlugin 是否可用
    if (typeof window.JSPlugin === 'undefined') {
      console.error('JSPlugin未定义');
      uni.showToast({
        title: 'WS协议播放器不可用',
        icon: 'none'
      });
      throw new Error('JSPlugin未定义');
    }

    // 3. 初始化播放器实例
    myPlugin = new window.JSPlugin({
      szId: 'play_window',                    // 播放容器 ID
      szBasePath: 'http://xxxxx.com/h5Player/', // 静态资源路径 (这里如果本地加载一定要与h5player.min.js的引用目录一致,/static/h5Player/)
      mseWorkerEnable: true,                  // 启用 Worker 提升性能
      bSupporDoubleClickFull: true,           // 支持双击全屏
    });

    // 4. 设置事件回调
    myPlugin.JS_SetWindowControlCallback({
      windowEventSelect: (index: number) => {
        curIndex = index;
        console.log('窗口选中:', index);
      },
      pluginErrorHandler: (index: number, iErrorCode: number, oError: any) => {
        console.error('播放器错误:', iErrorCode, oError);
        uni.showToast({ title: '播放失败', icon: 'none' });
      },
      firstFrameDisplay: (index: number, iWidth: number, iHeight: number) => {
        console.log('首帧显示:', index, iWidth, iHeight);
      },
      InterruptStream: (iWndIndex: number, interruptTime: number) => {
        console.log('断流事件:', iWndIndex, interruptTime);
      },
    });
  } catch (error) {
    console.error('初始化播放器失败:', error);
    uni.showToast({
      title: 'WS协议播放器初始化失败',
      icon: 'none'
    });
    throw error;
  }
};

配置参数说明

参数说明
szId容器 ID'play_window'
szBasePath资源基路径CDN 地址
mseWorkerEnableWorker 模式true (性能优化)
bSupporDoubleClickFull双击全屏true (用户体验)

3.3 视频播放控制

// src/pages/monitor/detail/index.vue:263-304

/**
 * 播放视频
 * @param url 视频流地址
 */
const playVideo = async (url: string) => {
  if (!myPlugin) {
    console.error('播放器未初始化');
    return;
  }

  try {
    await myPlugin.JS_Play(
      url,
      {
        playURL: url,           // 播放地址
        mode: 0,                // 播放模式: 0-实时播放
        PlayBackMode: 1,        // 回放模式: 1-实时
        keepDecoder: 0,         // 解码器配置
      },
      curIndex,                // 窗口索引
    );

    console.log('开始播放');
    hideLoading(); // 隐藏 loading
  } catch (err) {
    console.error('播放失败:', err);
    hideLoading();
    uni.showToast({
      title: '播放失败',
      icon: 'none'
    });
    // 可在此触发降级逻辑
    throw err;
  }
};

/**
 * 停止视频播放
 */
const stopVideo = async () => {
  if (!myPlugin) return;

  try {
    await myPlugin.JS_Stop(curIndex);
    console.log('停止播放');
  } catch (err) {
    console.error('停止失败:', err);
  }
};

此处 主要是按照海康官方文档一步一步实现。相关api在文档中都有写明

四、HLS 协议实现(降级方案)

4.1 HLS 播放器初始化

// src/pages/monitor/detail/index.vue:306-413
const initHlsPlayer = (url: string) => {
  console.log('初始化HLS播放器, URL:', url);

  // #ifdef H5
  // 1. 参数校验
  if (!url || url.trim() === '') {
    console.error('HLS URL 为空');
    uni.showToast({ title: 'HLS地址无效', icon: 'none' });
    return;
  }

  // 2. 获取视频元素
  const videoElement = videoRef.value?.$el || videoRef.value;
  console.log('视频元素类型:', videoElement);

  if (!videoElement) {
    console.error('视频元素未找到');
    uni.showToast({ title: '视频元素加载失败', icon: 'none' });
    return;
  }

  // 兼容不同环境的 video 获取方式
  const video = videoElement.tagName === 'VIDEO'
    ? videoElement
    : videoElement.querySelector('video');

  if (!video) {
    console.error('未找到video标签');
    uni.showToast({ title: '视频标签加载失败', icon: 'none' });
    return;
  }

  // 3. 浏览器能力检测与播放
  if (Hls.isSupported()) {
    // 3.1 使用 hls.js 库
    console.log('使用 hls.js 播放');
    hls = new Hls({
      debug: true,           // 调试模式
      enableWorker: true,    // 启用 Worker
      lowLatencyMode: true,  // 低延迟模式
    });

    hls.loadSource(url);
    hls.attachMedia(video);

    // 3.2 成功解析 manifest
    hls.on(Hls.Events.MANIFEST_PARSED, () => {
      console.log('HLS manifest 解析成功');
      video.play().catch((err) => {
        console.error('自动播放失败:', err);
        uni.showToast({
          title: '请手动点击播放',
          icon: 'none'
        });
      });
    });

    // 3.3 错误处理与恢复机制
    hls.on(Hls.Events.ERROR, (event, data) => {
      console.error('HLS 错误:', data);

      if (data.fatal) {
        switch (data.type) {
          case Hls.ErrorTypes.NETWORK_ERROR:
            console.error('网络错误,尝试恢复');
            hls?.startLoad(); // 重新加载
            break;
          case Hls.ErrorTypes.MEDIA_ERROR:
            console.error('媒体错误,尝试恢复');
            hls?.recoverMediaError(); // 恢复媒体错误
            break;
          default:
            console.error('无法恢复的错误');
            hls?.destroy();
            hls = null;
            uni.showToast({
              title: 'HLS播放失败',
              icon: 'none'
            });
            break;
        }
      }
    });
  }
  // 3.4 原生 HLS 支持(Safari、iOS)
  else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    console.log('使用原生 HLS 播放');
    video.src = url;

    video.addEventListener('loadedmetadata', () => {
      video.play().catch((err) => {
        console.error('播放失败:', err);
        uni.showToast({
          title: '请手动点击播放',
          icon: 'none'
        });
      });
    });

    video.addEventListener('error', (e) => {
      console.error('视频加载错误:', e);
      uni.showToast({
        title: '视频加载失败',
        icon: 'none'
      });
    });
  }
  // 3.5 完全不支持 HLS
  else {
    console.error('浏览器不支持 HLS');
    uni.showToast({
      title: '浏览器不支持HLS',
      icon: 'none'
    });
  }
  // #endif
};

HLS 错误类型处理

错误类型原因处理方式
NETWORK_ERROR网络中断/跨域startLoad() 重试
MEDIA_ERROR解码错误recoverMediaError() 恢复
OTHER_ERROR未知错误销毁实例,提示用户

五、双协议切换与降级策略

5.1 协议切换实现

// src/pages/monitor/detail/index.vue:415-464
const switchTab = async (tab: 'ws' | 'hls') => {
  if (activeTab.value === tab) return;

  // 1. 显示加载状态
  showLoading('监控连接中...');

  // 2. 清理当前协议资源
  if (activeTab.value === 'ws') {
    await stopVideo();
  } else if (activeTab.value === 'hls' && hls) {
    hls.destroy();
    hls = null;
  }

  // 3. 更新当前标签
  activeTab.value = tab;

  // 4. 初始化新协议
  if (tab === 'ws' && previewUrl.value) {
    try {
      await initPlayer();
      if (myPlugin && activeTab.value === 'ws') {
        playVideo(previewUrl.value);
      }
    } catch (error) {
      console.error('切换到WS协议失败:', error);
      hideLoading();
      uni.showToast({ title: '切换失败', icon: 'none' });
    }
  } else if (tab === 'hls') {
    if (!hlsUrl.value) {
      hideLoading();
      uni.showToast({ title: '未获取到HLS流地址', icon: 'none' });
      return;
    }
    setTimeout(() => {
      initHlsPlayer(hlsUrl.value);
      hideLoading();
    }, 100);
  }
};

5.2 页面初始化流程

// src/pages/monitor/detail/index.vue:466-520
const getDetail = async () => {
  try {
    // 1. 获取设备详情
    const res = await getMonitorDetail({
      indexCode: deviceCode.value,
    });
    deviceInfo.value = res.data || {};
    console.log('获取监控详情', res.data);

    // 2. 获取 WS 流地址(主协议)
    const wsStreamRes = await getMonitorStreamUrl({
      cameraIndexCode: deviceCode.value,
      protocol: 'ws',
      streamType: 0,
      transmode: 1,
    });

    previewUrl.value = wsStreamRes.data.url || '';
    console.log('获取WS流地址:', previewUrl.value);

    // 3. 预获取 HLS 地址(备用)
    // const hlsStreamRes = await getMonitorStreamUrl({
    //   cameraIndexCode: deviceCode.value,
    //   protocol: 'hls',
    //   streamType: 0,
    //   transmode: 1,
    // });
    // hlsUrl.value = hlsStreamRes.data.url || '';

    // 4. 流地址有效性检查
    if (!previewUrl.value) {
      uni.showToast({ title: '获取视频流失败', icon: 'none' });
      return;
    }

    // 5. 初始化播放器
    await initPlayer();
    if (myPlugin) {
      playVideo(previewUrl.value);
    }
  } catch (error) {
    console.error('获取监控信息失败:', error);
    uni.showToast({ title: '加载失败,请重试', icon: 'none' });
    hideLoading();
  }
};

// 页面生命周期
onLoad(() => {
  // #ifndef MP-WEIXIN
  setNavTitle('查看监控');
  // #endif

  deviceCode.value = route.query.code as string || '';

  // 时间显示
  updateTime();
  timer = setInterval(updateTime, 1000);

  // 显示 loading 并开始加载
  showLoading('监控连接中...');
  getDetail();
});

5.3 降级策略实现

5.3.1 主动降级(推荐)

// 方案:初始化阶段检测并降级
const initPlayer = async () => {
  try {
    await loadJSPluginScript();
    // ... 初始化逻辑
  } catch (error) {
    // 自动降级到 HLS
    console.error('WS协议不可用,自动降级到HLS');

    // 如果没有 HLS 地址,需要重新获取
    if (!hlsUrl.value) {
      await fetchHLSUrl();
    }

    if (hlsUrl.value) {
      activeTab.value = 'hls';
      initHlsPlayer(hlsUrl.value);
    } else {
      uni.showToast({
        title: '暂无可播放的视频流',
        icon: 'none'
      });
    }
  }
};

// 获取 HLS 地址
const fetchHLSUrl = async () => {
  try {
    const hlsStreamRes = await getMonitorStreamUrl({
      cameraIndexCode: deviceCode.value,
      protocol: 'hls',
      streamType: 0,
      transmode: 1,
    });
    hlsUrl.value = hlsStreamRes.data.url || '';
  } catch (error) {
    console.error('获取HLS地址失败:', error);
  }
};

5.3.2 播放失败降级

// 播放失败时自动切换
const playVideo = async (url: string) => {
  if (!myPlugin) return;

  try {
    await myPlugin.JS_Play(url, { ... }, curIndex);
  } catch (err) {
    console.error('WS播放失败,尝试HLS降级');

    // 确保有 HLS 地址
    if (!hlsUrl.value) {
      await fetchHLSUrl();
    }

    if (hlsUrl.value) {
      activeTab.value = 'hls';
      initHlsPlayer(hlsUrl.value);
    }
  }
};

5.3.3 预加载备用方案

// 在获取 WS 地址时,同时获取 HLS 地址
const getDetail = async () => {
  try {
    // 并行请求两个协议的流地址
    const [wsRes, hlsRes] = await Promise.allSettled([
      getMonitorStreamUrl({
        cameraIndexCode: deviceCode.value,
        protocol: 'ws',
        streamType: 0,
        transmode: 1,
      }),
      getMonitorStreamUrl({
        cameraIndexCode: deviceCode.value,
        protocol: 'hls',
        streamType: 0,
        transmode: 1,
      })
    ]);

    // 处理 WS 结果
    if (wsRes.status === 'fulfilled') {
      previewUrl.value = wsRes.value.data.url || '';
    }

    // 处理 HLS 结果
    if (hlsRes.status === 'fulfilled') {
      hlsUrl.value = hlsRes.value.data.url || '';
    }

    // 优先使用 WS
    if (previewUrl.value) {
      await initPlayer();
      if (myPlugin) {
        playVideo(previewUrl.value);
      }
    } else if (hlsUrl.value) {
      // WS 失败,使用 HLS
      activeTab.value = 'hls';
      initHlsPlayer(hlsUrl.value);
    } else {
      uni.showToast({ title: '获取视频流失败', icon: 'none' });
    }
  } catch (error) {
    console.error('获取监控信息失败:', error);
    uni.showToast({ title: '加载失败,请重试', icon: 'none' });
  }
};

六、资源清理与内存管理

6.1 资源泄漏风险点

风险点影响解决方案
定时器未清理内存泄漏onHide/onUnload 清除
播放器实例未销毁Socket 连接持续显式调用销毁方法
HLS 事件未解绑事件监听器累积destroy() 自动解绑
DOM 引用残留无法 GC置为 null
WebSocket 连接后台持续消耗JS_Stop/JS_Destroy

6.2 页面隐藏时的清理(onHide)

// src/pages/monitor/detail/index.vue:543-576
onHide(async () => {
  console.log('页面隐藏,清理资源');

  // 1. 清理定时器
  if (timer) {
    clearInterval(timer);
    timer = null;
  }

  // 2. 隐藏 loading
  hideLoading();

  // 3. 停止 WS 播放
  await stopVideo();

  // 4. 销毁 WS 播放器实例
  if (myPlugin) {
    console.log('销毁WS播放器实例');
    try {
      // 确保调用 JS_Stop 停止播放
      if (typeof myPlugin.JS_Stop === 'function') {
        await myPlugin.JS_Stop(curIndex);
        console.log('调用JS_Stop停止播放成功');
      }
    } catch (error) {
      console.error('销毁播放器实例失败:', error);
    } finally {
      // 确保清空实例引用
      myPlugin = null;
    }
  }

  // 5. 销毁 HLS 实例
  if (hls) {
    console.log('销毁 HLS 实例');
    hls.destroy();
    hls = null;
  }
});

6.3 页面卸载时的清理(onUnload)

// src/pages/monitor/detail/index.vue:578-630
onUnload(async () => {
  console.log('页面卸载,清理资源');

  // 1. 清理定时器
  if (timer) {
    clearInterval(timer);
    timer = null;
  }

  // 2. 隐藏 loading
  hideLoading();

  // 3. 停止播放
  await stopVideo();

  // 4. 销毁 WS 播放器
  if (myPlugin) {
    console.log('销毁WS播放器实例');
    try {
      // 尝试多种销毁方法
      if (typeof myPlugin.JS_Destroy === 'function') {
        myPlugin.JS_Destroy();
      } else if (typeof myPlugin.JS_Uninit === 'function') {
        myPlugin.JS_Uninit();
      }
    } catch (error) {
      console.error('销毁播放器实例失败:', error);
    }
    myPlugin = null;
  }

  // 5. 销毁 HLS
  if (hls) {
    console.log('销毁 HLS 实例');
    hls.destroy();
    hls = null;
  }
});

清理顺序

  1. 停止播放 → 2. 销毁实例 → 3. 清除引用 → 4. 清理定时器

七、降级处理策略详解

7.1 降级触发场景矩阵

降级场景触发条件降级方案用户提示
脚本加载失败CDN 超时/跨域/网络错误切换到 HLS"WS协议播放器加载失败"
播放器初始化失败JSPlugin 未定义/初始化异常切换到 HLS"WS协议播放器不可用"
视频流获取失败API 返回空地址/网络错误提示错误"获取视频流失败"
播放失败JS_Play 调用失败切换到 HLS"播放失败"
HLS 播放失败manifest 解析错误网络错误尝试恢复"HLS播放失败"
浏览器不支持无 MSE/原生 HLS提示不支持"浏览器不支持HLS"

7.2 降级流程图

graph TD
    A[开始初始化] --> B{JSPlugin加载成功?}
    B -->|是| C[初始化WS播放器]
    B -->|否| D[获取HLS地址]
    C --> E{初始化成功?}
    E -->|是| F[播放视频]
    E -->|否| D
    D --> G{HLS地址有效?}
    G -->|是| H[初始化HLS播放器]
    G -->|否| I[提示错误]
    H --> J{HLS播放成功?}
    J -->|是| K[正常播放]
    J -->|否| I
    F --> K
    I --> L[结束]
    K --> L

7.3 降级代码实现

// 完整的降级处理逻辑
const initializeVideoPlayer = async () => {
  // 1. 尝试 WebSocket 协议
  try {
    console.log('尝试初始化 WebSocket 协议...');
    await initPlayer();

    // 检查播放器是否可用
    if (myPlugin && previewUrl.value) {
      await playVideo(previewUrl.value);
      console.log('WebSocket 协议初始化成功');
      return; // 成功,直接返回
    }

    throw new Error('播放器不可用');
  } catch (wsError) {
    console.warn('WebSocket 协议失败:', wsError);

    // 2. 尝试 HLS 协议
    try {
      console.log('尝试初始化 HLS 协议...');

      // 获取 HLS 地址(如果未预获取)
      if (!hlsUrl.value) {
        const hlsRes = await getMonitorStreamUrl({
          cameraIndexCode: deviceCode.value,
          protocol: 'hls',
          streamType: 0,
          transmode: 1,
        });
        hlsUrl.value = hlsRes.data.url || '';
      }

      if (!hlsUrl.value) {
        throw new Error('未获取到 HLS 流地址');
      }

      // 切换到 HLS 标签
      activeTab.value = 'hls';

      // 初始化 HLS 播放器
      await new Promise((resolve, reject) => {
        try {
          initHlsPlayer(hlsUrl.value);
          // HLS 是异步的,稍后检查
          setTimeout(() => {
            const video = document.querySelector('video');
            if (video && video.readyState >= 2) {
              resolve(true);
            } else {
              reject(new Error('HLS 初始化超时'));
            }
          }, 2000);
        } catch (error) {
          reject(error);
        }
      });

      console.log('HLS 协议初始化成功');
    } catch (hlsError) {
      console.error('HLS 协议也失败:', hlsError);

      // 3. 完全失败,提示用户
      uni.showToast({
        title: '视频播放失败,请稍后重试',
        icon: 'none',
        duration: 3000
      });

      // 可选:记录错误日志
      // reportError(wsError, hlsError);
    } finally {
      hideLoading();
    }
  }
};

7.4 降级用户体验优化

// 1. Loading 状态管理
const showLoading = (text = '加载中...') => {
  uni.showLoading({ title: text, mask: true });
};

const hideLoading = () => {
  uni.hideLoading();
};

// 2. 分阶段提示
const阶段性提示 = async () => {
  showLoading('正在连接...');

  try {
    // 阶段1: 获取设备信息
    showLoading('获取设备信息...');
    await getDetail();

    // 阶段2: 初始化播放器
    showLoading('初始化播放器...');
    await initializeVideoPlayer();

  } catch (error) {
    // 阶段3: 错误处理
    console.error('播放流程失败:', error);
    uni.showToast({
      title: '视频加载失败',
      icon: 'none'
    });
  } finally {
    hideLoading();
  }
};

// 3. 错误日志记录
const logPlaybackError = (stage: string, error: any, context: any) => {
  console.error(`[${stage}] 错误详情:`, {
    error: error.message || error,
    context,
    timestamp: new Date().toISOString(),
    userAgent: navigator.userAgent,
    url: window.location.href
  });

  // 可选:上报到监控平台
  // reportToMonitoring({
  //   stage,
  //   error: error.message,
  //   deviceCode: deviceCode.value,
  //   timestamp: Date.now()
  // });
};

八、平台兼容性处理

8.1 条件编译指令

<!-- 8.1.1 H5 平台专用结构 -->
<!-- #ifdef H5 -->
<div v-show="activeTab === 'hls'" class="hls-container">
  <video
    ref="videoRef"
    class="hls-video"
    controls
    autoplay
    muted
    playsinline
  />
</div>
<!-- #endif -->

<!-- 8.1.2 非 H5 平台(小程序、App) -->
<!-- #ifndef H5 -->
<video
  v-show="activeTab === 'hls'"
  class="hls-video"
  :src="hlsUrl"
  controls
  autoplay
  muted
/>
<!-- #endif -->

<!-- 8.1.3 非微信小程序平台 -->
<!-- #ifndef MP-WEIXIN -->
<view class="tab-container">
  <!-- Tab 切换控件 -->
</view>
<!-- #endif -->

8.2 VConsole 处理(H5 开发环境)

// 仅在 H5 开发环境启用 VConsole
// #ifdef H5
if (import.meta.env.DEV && import.meta.env.VITE_APP_ENV === 'development') {
  import('vconsole').then((VConsole) => {
    new VConsole.default();
    console.log('VConsole 已启用');
  });
}
// #endif

九、关键代码片段汇总

9.1 核心状态管理

// 播放器实例
let myPlugin: any = null;        // WebSocket 播放器
let hls: Hls | null = null;      // HLS 播放器

// 视频流地址
const previewUrl = ref<string>('');  // WS 地址
const hlsUrl = ref<string>('');      // HLS 地址

// 当前状态
const activeTab = ref<'ws' | 'hls'>('ws');  // 当前协议
const deviceInfo = ref<deviceType>();       // 设备信息
const currentTime = ref('');                // 当前时间
let timer: any = null;                      // 时间定时器

// DOM 引用
const videoRef = ref();  // HLS 视频元素

9.2 完整生命周期管理

// 页面加载
onLoad(() => {
  deviceCode.value = route.query.code as string || '';

  // 时间显示
  updateTime();
  timer = setInterval(updateTime, 1000);

  // 开始加载
  showLoading('监控连接中...');
  getDetail();
});

// 页面显示
onShow(() => {
  // 可用于恢复播放
  console.log('页面显示');
});

// 页面隐藏
onHide(async () => {
  console.log('页面隐藏,清理资源');
  if (timer) clearInterval(timer);
  hideLoading();
  await stopVideo();
  if (myPlugin) myPlugin = null;
  if (hls) {
    hls.destroy();
    hls = null;
  }
});

// 页面卸载
onUnload(async () => {
  console.log('页面卸载,清理资源');
  if (timer) clearInterval(timer);
  hideLoading();
  await stopVideo();
  if (myPlugin) {
    try {
      if (typeof myPlugin.JS_Destroy === 'function') {
        myPlugin.JS_Destroy();
      }
    } catch (error) {
      console.error('销毁失败:', error);
    }
    myPlugin = null;
  }
  if (hls) {
    hls.destroy();
    hls = null;
  }
});

十、方案优缺点分析

10.1 优势(Pros)

✅ 技术优势

  1. 双协议架构,可靠性高

    • WebSocket 作为主协议,提供低延迟体验
    • HLS 作为降级方案,确保在任何情况下都能播放
    • 失败自动切换,用户无感知
  2. 动态加载,性能优化

    • JSPlugin 按需加载,减少首屏资源体积
    • 避免不必要的脚本加载,提升页面加载速度
    • 仅在 H5 环境加载,其他平台无负担
  3. 完善的资源管理

    • 页面隐藏/卸载时彻底清理资源
    • 防止内存泄漏和后台连接消耗
    • 多重保障确保 Socket 连接关闭
  4. 错误处理完善

    • 多层错误捕获(加载、初始化、播放)
    • 详细的错误日志记录
    • 用户友好的提示信息
  5. 平台兼容性好

    • 使用 uni-app 条件编译
    • H5、小程序、App 多端支持
    • 根据平台自动选择最佳协议

✅ 业务优势

  1. 用户体验流畅

    • Loading 状态明确提示
    • 降级过程对用户透明
    • 支持手动切换协议
  2. 可维护性强

    • 代码结构清晰,职责分离
    • 配置集中管理
    • 易于扩展新协议

10.2 缺点(Cons)

❌ 技术局限性

  1. WebSocket 协议依赖性强

    • 问题:严重依赖 JSPlugin.js,该脚本必须通过 CDN 加载
    • 影响
      • CDN 不可用时完全无法使用 WebSocket
      • 跨域问题可能导致加载失败
      • 网络环境差时加载超时
    • 缓解:HLS 降级方案
  2. HLS 协议延迟较高

    • 问题:HLS 延迟通常在 3-10 秒,不适合实时性要求高的场景
    • 影响
      • 用户操作反馈慢
      • 不适合需要快速响应的监控场景
    • 缓解:使用 WebSocket 作为首选
  3. 平台限制明显

    • 问题:JSPlugin 仅支持 H5 平台
    • 影响
      • 微信小程序无法使用 WebSocket
      • App 端需要原生开发或额外 SDK
      • 跨平台一致性差
    • 缓解:小程序强制使用 HLS,App 端可考虑原生方案
  4. 浏览器兼容性问题

    • 问题:HLS.js 依赖 MSE (Media Source Extensions)
    • 影响
      • IE11 及以下完全不支持
      • 部分国产浏览器内核不完整
      • iOS Safari 需要原生支持
    • 缓解:多重降级策略

十一、 对接过程遇到的问题

11.1 hls个别视频无法播放

解决方案:确实海康平台视频编码是否为h264,hls协议对编码格式要求高,最好是视频编码h264,若有音频 最好ACC格式,而且推流的视频不保证100%播放,有问题可以询问海康官方对接群。

11.2 ws协议视频无法播放

解决方案:确认控制台是否报错,因为海康用官方插件的报错几乎在控制台都有错误码,去海康开放平台发给在线ai客服 几乎都能找到解决方案。我这遇到的主要就是媒体网关问题,要更高的版本,要找运维与海康那边对接升级。

11.3 https或者wss无法播放

解决方案:1.确认海康那边是否支持,2. 去海康申请媒体的ssl证书,在海康的运营中心上传证书。无法播放九成就是证书的问题

11.4 vpn权限

当时同事对接的时候一直无法播放,最后发现vpn权限不足的话也会影响到视频的正常取流播放。因为甲方监控部署在内网中。

十二、 nginx部署

因为海康插件包太大,要上政府平台会因为体积包太大无法上传,此时就要提前先上传到服务器,然后通过src远程加载 你可以先放置在与项目平级的目录中,我的是单独创建了plugins中, 在这里插入图片描述 在这里插入图片描述 nginx映射的时候要注意,海康插件在h264都是正常的,但是检测到h265视频编码时,会自动去兼容decoderWorker文件播放,这时候要后端映射好nginx文件,否则会控制台报跨域问题 在这里插入图片描述

十三、总结

13.1 方案核心价值

一句话总结:通过 WebSocket + HLS 双协议架构,配合完善的降级策略和资源管理,实现海康监控在 uni-app 中的稳定播放。

三大核心

  1. 可靠性:双协议互为备份,确保 99% 的可用性
  2. 性能:动态加载 + 资源清理,保证流畅体验
  3. 兼容性:多平台适配,覆盖主流场景

13.2 适用场景

场景推荐度说明
H5 监控页面⭐⭐⭐⭐⭐完美支持,体验最佳
微信小程序⭐⭐⭐⭐使用 HLS,延迟可接受
App 内嵌 H5⭐⭐⭐⭐依赖 WebView 能力
跨平台应用⭐⭐⭐需要额外适配工作
实时性要求极高⭐⭐建议原生开发

C. 参考资料