Skip to content

完整使用示例

vue
<template>
  <div class="app-container">
    <div ref="containerRef" class="device-container"></div>
    <div class="controls">
      <div class="input-group">
        <label class="label">主机IP</label>
        <input v-model="hostIp" placeholder="例如: 192.168.10.50" class="input" />
        <label class="label">云机ID (Device Name)</label>
        <input v-model="deviceName" placeholder="例如: EDGE00M7EHBKZGVN" class="input" />
      </div>
      <div class="button-group">
        <button @click="connect" class="btn btn-primary">连接</button>
        <button @click="disconnect" class="btn btn-secondary">断开</button>
        <button @click="handleHome" class="btn btn-action">Home</button>
        <button @click="handleBack" class="btn btn-action">Back</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, shallowRef, markRaw, onMounted, onUnmounted } from "vue";
import {
  VmosEdgeClient,
  VmosEdgeClientEvents,
  type VmosEdgeClientConfig,
  type VmosEdgeErrorEvent,
} from "@vmosedge/web-sdk";

const containerRef = ref<HTMLElement | null>(null);
const client = shallowRef<VmosEdgeClient | null>(null);
const hostIp = ref("192.168.10.50"); // 主机IP
const deviceName = ref("EDGE00M7EHBKZGVN"); // 云机ID

// 接口返回数据类型定义
interface DeviceInfo {
  ip: string;
  host_ip: string;
  db_id: string;
  network_mode: string;
  is_macvlan: boolean;
  tcp_port: number;
  tcp_control_port: number;
}

interface ApiResponse {
  code: number;
  msg: string;
  data: {
    count: number;
    host_ip: string;
    list: DeviceInfo[];
  };
}

// 获取设备信息并构建配置
const getDeviceConfig = async (
  hostIp: string,
  deviceName: string
): Promise<VmosEdgeClientConfig> => {
  const response = await fetch(`http://${hostIp}:18182/container_api/v1/get_db`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ name: deviceName }),
  });

  const result: ApiResponse = await response.json();

  if (result.code !== 200 || !result.data.list || result.data.list.length === 0) {
    throw new Error(result.msg || "获取设备信息失败");
  }

  const device = result.data.list[0];
  const isMacvlan = device.network_mode === "macvlan" || device.is_macvlan;

  // 根据网络模式构建配置
  if (isMacvlan) {
    // 局域网模式(macvlan)
    return {
      ip: device.ip, // 使用设备的 ip 字段
      deviceId: device.db_id, // 使用设备的 db_id 字段
      ports: {
        video: 9999, // 局域网模式固定端口
        touch: 9997, // 局域网模式固定端口
      },
    };
  } else {
    // 非局域网模式(bridge)
    return {
      ip: result.data.host_ip, // 使用 data 层级的 host_ip 字段
      deviceId: device.db_id, // 使用设备的 db_id 字段
      ports: {
        video: device.tcp_port, // 使用设备的 tcp_port 字段
        touch: device.tcp_control_port, // 使用设备的 tcp_control_port 字段
      },
    };
  }
};

const connect = async () => {
  if (!containerRef.value) return;

  // 环境检测
  if (!VmosEdgeClient.isWebCodecsSupported()) {
    console.error("当前环境不支持 SDK 使用,请使用 file:// 协议打开 HTML 文件,或通过 localhost 访问");
    return;
  }

  try {
    // 获取设备配置
    const config = await getDeviceConfig(hostIp.value, deviceName.value);

    const instance = new VmosEdgeClient({
      container: containerRef.value,
      config,
      retryCount: 3,
    });

    // 使用 markRaw 避免 Vue 响应式代理
    client.value = markRaw(instance);

    // 监听事件
    client.value.on(VmosEdgeClientEvents.STARTED, () => {
      console.log("连接成功");
    });

    client.value.on(VmosEdgeClientEvents.ERROR, (error: VmosEdgeErrorEvent) => {
      console.error("错误:", error);
    });

    await client.value.start();
  } catch (error) {
    console.error("连接失败:", error);
  }
};

const disconnect = () => {
  client.value?.stop();
  client.value = null;
};

const handleHome = () => {
  client.value?.home();
};

const handleBack = () => {
  client.value?.back();
};

onUnmounted(() => {
  disconnect();
});
</script>

<style scoped>
.app-container {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 60px;
  padding: 40px;
  height: 100vh;
  overflow: hidden;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  background: #fff;
}

.device-container {
  width: 360px;
  height: 720px;
  background: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  flex-shrink: 0;
  overflow: hidden;
}

.controls {
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: 320px;
  flex-shrink: 0;
  max-height: 100vh;
  overflow: visible;
}

.input-group {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 0;
}

.input {
  padding: 12px 16px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  font-size: 14px;
  transition: all 0.2s;
  outline: none;
  width: 100%;
  background: #fff;
}

.input:focus {
  border-color: #4a90e2;
  box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}

.button-group {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.btn {
  padding: 12px 20px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
  outline: none;
  min-width: 0;
}

.btn-primary {
  background: #4a90e2;
  color: white;
  grid-column: 1 / -1;
}

.btn-primary:hover {
  background: #357abd;
}

.btn-secondary {
  background: #6c757d;
  color: white;
}

.btn-secondary:hover {
  background: #5a6268;
}

.btn-action {
  background: #f8f9fa;
  color: #333;
  border: 1px solid #dee2e6;
}

.btn-action:hover {
  background: #e9ecef;
}
</style>
tsx
import { useEffect, useRef, useState } from "react";
import {
  VmosEdgeClient,
  VmosEdgeClientEvents,
  type VmosEdgeClientConfig,
  type VmosEdgeErrorEvent,
} from "@vmosedge/web-sdk";

// 接口返回数据类型定义
interface DeviceInfo {
  ip: string;
  host_ip: string;
  db_id: string;
  network_mode: string;
  is_macvlan: boolean;
  tcp_port: number;
  tcp_control_port: number;
}

interface ApiResponse {
  code: number;
  msg: string;
  data: {
    count: number;
    host_ip: string;
    list: DeviceInfo[];
  };
}

function App() {
  const containerRef = useRef<HTMLDivElement>(null);
  const clientRef = useRef<VmosEdgeClient | null>(null);
  const [connected, setConnected] = useState(false);
  const [hostIp, setHostIp] = useState("192.168.10.50"); // 主机IP
  const [deviceName, setDeviceName] = useState("EDGE00M7EHBKZGVN"); // 云机ID

  // 获取设备信息并构建配置
  const getDeviceConfig = async (
    hostIp: string,
    deviceName: string
  ): Promise<VmosEdgeClientConfig> => {
    const response = await fetch(`http://${hostIp}:18182/container_api/v1/get_db`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name: deviceName }),
    });

    const result: ApiResponse = await response.json();

    if (result.code !== 200 || !result.data.list || result.data.list.length === 0) {
      throw new Error(result.msg || "获取设备信息失败");
    }

    const device = result.data.list[0];
    const isMacvlan = device.network_mode === "macvlan" || device.is_macvlan;

    // 根据网络模式构建配置
    if (isMacvlan) {
      // 局域网模式(macvlan)
      return {
        ip: device.ip, // 使用设备的 ip 字段
        deviceId: device.db_id, // 使用设备的 db_id 字段
        ports: {
          video: 9999, // 局域网模式固定端口
          touch: 9997, // 局域网模式固定端口
        },
      };
    } else {
      // 非局域网模式(bridge)
      return {
        ip: result.data.host_ip, // 使用 data 层级的 host_ip 字段
        deviceId: device.db_id, // 使用设备的 db_id 字段
        ports: {
          video: device.tcp_port, // 使用设备的 tcp_port 字段
          touch: device.tcp_control_port, // 使用设备的 tcp_control_port 字段
        },
      };
    }
  };

  const connect = async () => {
    if (!containerRef.current) return;

    // 环境检测
    if (!VmosEdgeClient.isWebCodecsSupported()) {
      console.error("当前环境不支持 SDK 使用,请使用 file:// 协议打开 HTML 文件,或通过 localhost 访问");
      return;
    }

    try {
      // 获取设备配置
      const config = await getDeviceConfig(hostIp, deviceName);

      const client = new VmosEdgeClient({
        container: containerRef.current,
        config,
        retryCount: 3,
      });

      clientRef.current = client;

      client.on(VmosEdgeClientEvents.STARTED, () => {
        setConnected(true);
        console.log("连接成功");
      });

      client.on(VmosEdgeClientEvents.ERROR, (error: VmosEdgeErrorEvent) => {
        console.error("错误:", error);
      });

      await client.start();
    } catch (error) {
      console.error("连接失败:", error);
    }
  };

  const disconnect = () => {
    clientRef.current?.stop();
    clientRef.current = null;
    setConnected(false);
  };

  useEffect(() => {
    return () => {
      disconnect();
    };
  }, []);

  return (
    <div style={styles.container}>
      <div ref={containerRef} style={styles.deviceContainer} />
      <div style={styles.controls}>
        <div style={styles.inputGroup}>
          <label style={styles.label}>主机IP</label>
          <input
            value={hostIp}
            onChange={(e) => setHostIp(e.target.value)}
            placeholder="例如: 192.168.10.50"
            style={styles.input}
          />
          <label style={styles.label}>云机ID (Device Name)</label>
          <input
            value={deviceName}
            onChange={(e) => setDeviceName(e.target.value)}
            placeholder="例如: EDGE00M7EHBKZGVN"
            style={styles.input}
          />
        </div>
        <div style={styles.buttonGroup}>
          <button onClick={connect} style={{ ...styles.btn, ...styles.btnPrimary }}>
            连接
          </button>
          <button onClick={disconnect} style={{ ...styles.btn, ...styles.btnSecondary }}>
            断开
          </button>
          <button onClick={() => clientRef.current?.home()} style={{ ...styles.btn, ...styles.btnAction }}>
            Home
          </button>
          <button onClick={() => clientRef.current?.back()} style={{ ...styles.btn, ...styles.btnAction }}>
            Back
          </button>
        </div>
      </div>
    </div>
  );
}

const styles = {
  container: {
    display: "flex",
    flexDirection: "row" as const,
    alignItems: "center",
    justifyContent: "center",
    gap: "40px",
    padding: "20px",
    minHeight: "100vh",
    maxHeight: "100vh",
    overflow: "hidden",
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
  },
  deviceContainer: {
    width: 360,
    height: 720,
    background: "#fff",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    borderRadius: "12px",
    boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
    flexShrink: 0,
    overflow: "hidden",
  },
  controls: {
    display: "flex",
    flexDirection: "column" as const,
    gap: "16px",
    width: "360px",
    flexShrink: 0,
  },
  inputGroup: {
    display: "flex",
    flexDirection: "column" as const,
    gap: "8px",
  },
  label: {
    fontSize: "14px",
    fontWeight: 500,
    color: "#333",
    marginBottom: "4px",
  },
  input: {
    padding: "12px 16px",
    border: "1px solid #e0e0e0",
    borderRadius: "6px",
    fontSize: "14px",
    transition: "all 0.2s",
    outline: "none",
  },
  buttonGroup: {
    display: "flex",
    gap: "8px",
    flexWrap: "wrap" as const,
  },
  btn: {
    padding: "10px 20px",
    border: "none",
    borderRadius: "6px",
    fontSize: "14px",
    fontWeight: 500,
    cursor: "pointer",
    transition: "all 0.2s",
    outline: "none",
  },
  btnPrimary: {
    background: "#4a90e2",
    color: "white",
    flex: 1,
  },
  btnSecondary: {
    background: "#6c757d",
    color: "white",
    flex: 1,
  },
  btnAction: {
    background: "#f8f9fa",
    color: "#333",
    border: "1px solid #dee2e6",
  },
};

export default App;
html
<!DOCTYPE html>
<html>
<head>
  <title>Vmos Edge Web SDK 示例</title>
  <style>
    * {
      box-sizing: border-box;
    }
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
      overflow: hidden;
    }
    body {
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      gap: 40px;
      min-height: 100vh;
      max-height: 100vh;
      background: #f5f5f5;
    }
    #container {
      width: 360px;
      height: 720px;
      background: #fff;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      flex-shrink: 0;
      overflow: hidden;
    }
    .controls {
      display: flex;
      flex-direction: column;
      gap: 16px;
      width: 360px;
      flex-shrink: 0;
    }
    .input-group {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .input-group label {
      font-size: 14px;
      font-weight: 500;
      color: #333;
      margin-bottom: 4px;
    }
    .input-group input {
      padding: 12px 16px;
      border: 1px solid #e0e0e0;
      border-radius: 6px;
      font-size: 14px;
      transition: all 0.2s;
      outline: none;
    }
    .input-group input:focus {
      border-color: #4a90e2;
      box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
    }
    .button-group {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }
    .button-group button {
      padding: 10px 20px;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
      outline: none;
    }
    .button-group button:hover {
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    .btn-primary {
      background: #4a90e2;
      color: white;
      flex: 1;
    }
    .btn-primary:hover {
      background: #357abd;
    }
    .btn-secondary {
      background: #6c757d;
      color: white;
      flex: 1;
    }
    .btn-secondary:hover {
      background: #5a6268;
    }
    .btn-action {
      background: #f8f9fa;
      color: #333;
      border: 1px solid #dee2e6;
    }
    .btn-action:hover {
      background: #e9ecef;
    }
  </style>
</head>
<body>
  <div id="container"></div>
  <div class="controls">
    <div class="input-group">
      <label>主机IP</label>
      <input id="hostIp" type="text" placeholder="例如: 192.168.10.50" value="192.168.10.50" />
      <label>云机ID (Device Name)</label>
      <input id="deviceName" type="text" placeholder="例如: EDGE00M7EHBKZGVN" value="EDGE00M7EHBKZGVN" />
    </div>
    <div class="button-group">
      <button onclick="connect()" class="btn-primary">连接</button>
      <button onclick="disconnect()" class="btn-secondary">断开</button>
      <button onclick="handleHome()" class="btn-action">Home</button>
      <button onclick="handleBack()" class="btn-action">Back</button>
    </div>
  </div>

  <script type="module">
    import {
      VmosEdgeClient,
      VmosEdgeClientEvents,
    } from "@vmosedge/web-sdk";

    let client = null;

    // 获取设备信息并构建配置
    async function getDeviceConfig(hostIp, deviceName) {
      const response = await fetch(`http://${hostIp}:18182/container_api/v1/get_db`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ name: deviceName }),
      });

      const result = await response.json();

      if (result.code !== 200 || !result.data.list || result.data.list.length === 0) {
        throw new Error(result.msg || "获取设备信息失败");
      }

      const device = result.data.list[0];
      const isMacvlan = device.network_mode === "macvlan" || device.is_macvlan;

      // 根据网络模式构建配置
      if (isMacvlan) {
        // 局域网模式(macvlan)
        return {
          ip: device.ip, // 使用设备的 ip 字段
          deviceId: device.db_id, // 使用设备的 db_id 字段
          ports: {
            video: 9999, // 局域网模式固定端口
            touch: 9997, // 局域网模式固定端口
          },
        };
      } else {
        // 非局域网模式(bridge)
        return {
          ip: result.data.host_ip, // 使用 data 层级的 host_ip 字段
          deviceId: device.db_id, // 使用设备的 db_id 字段
          ports: {
            video: device.tcp_port, // 使用设备的 tcp_port 字段
            touch: device.tcp_control_port, // 使用设备的 tcp_control_port 字段
          },
        };
      }
    }

    async function connect() {
      const container = document.getElementById("container");
      const hostIp = document.getElementById("hostIp").value;
      const deviceName = document.getElementById("deviceName").value;

      // 环境检测
      if (!VmosEdgeClient.isWebCodecsSupported()) {
        console.error("当前环境不支持 SDK 使用,请使用 file:// 协议打开 HTML 文件,或通过 localhost 访问");
        return;
      }

      try {
        // 获取设备配置
        const config = await getDeviceConfig(hostIp, deviceName);

        client = new VmosEdgeClient({
          container,
          config,
          retryCount: 3,
        });

        client.on(VmosEdgeClientEvents.STARTED, () => {
          console.log("连接成功");
        });

        client.on(VmosEdgeClientEvents.ERROR, (error) => {
          console.error("错误:", error);
        });

        await client.start();
      } catch (error) {
        console.error("连接失败:", error);
      }
    }

    function disconnect() {
      if (client) {
        client.stop();
        client = null;
      }
    }

    function handleHome() {
      client?.home();
    }

    function handleBack() {
      client?.back();
    }

    // 导出到全局作用域
    window.connect = connect;
    window.disconnect = disconnect;
    window.handleHome = handleHome;
    window.handleBack = handleBack;
  </script>
</body>
</html>

⚠️ 注意事项

Vue/React 框架使用

重要: 在 Vue 或 React 中使用时,VmosEdgeClient 实例不应被深度代理。

  • Vue 3:使用 shallowRefmarkRaw
  • React:使用 useRef

错误示例(Vue):

typescript
// ❌ 错误:使用 ref 会导致 Proxy 代理问题
const client = ref<VmosEdgeClient | null>(null);
client.value = new VmosEdgeClient({ /* ... */ });

正确示例(Vue):

typescript
// ✅ 正确:使用 shallowRef 和 markRaw
const client = shallowRef<VmosEdgeClient | null>(null);
const instance = new VmosEdgeClient({ /* ... */ });
client.value = markRaw(instance);

容器元素

  • 容器元素必须是有效的 HTMLElement
  • SDK 会在容器内创建 .vmos-canvas-container 元素
  • 建议为容器设置固定尺寸,避免布局问题

连接配置

  • 单控模式videotouch 端口都是必填
  • 群控模式:从设备仅需要 touch 端口,不需要 video 端口
  • audio 端口暂不支持,请勿配置
  • 确保设备 IP 和端口配置正确

错误处理

  • 务必监听 ERROR 事件,及时处理连接错误
  • 根据错误类型(error.type)和错误代码(error.code)进行相应处理

资源清理

  • 在组件卸载或页面关闭时,务必调用 client.stop() 释放资源
  • 避免内存泄漏

VMOS Edge 团队出品