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

2025博客之星年度评选已开启 10w+人浏览 1.9k人参与

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


一、技术选型与架构设计

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 (Web 端 HLS 播放器)

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

1.3 工具准备

1.3.1 海康官方插件包

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

1.3.2 hls协议实现所需工具

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

二、核心实现流程

2.1 完整业务流程

用户 监控详情页 后端服务 播放器 点击监控设备 显示 Loading 获取设备详情 返回设备信息 请求 WS 流地址 返回 WS URL 动态加载 JSPlugin 初始化播放器 开始播放 隐藏 Loading 请求 HLS 流地址 返回 HLS URL 初始化 HLS 播放器 隐藏 Loading alt [加载成功] [加载失败] 用户 监控详情页 后端服务 播放器

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 降级流程图

开始初始化
JSPlugin加载成功?
初始化WS播放器
获取HLS地址
初始化成功?
播放视频
HLS地址有效?
初始化HLS播放器
提示错误
HLS播放成功?
正常播放
结束

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. 参考资料


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码贫困户

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值