Bläddra i källkod

master: Fixed 对讲功能更换

gitboyzcf 4 månader sedan
förälder
incheckning
59a7c78f72
7 ändrade filer med 454 tillägg och 148 borttagningar
  1. 2 0
      .env.development
  2. 3 0
      .env.production
  3. 2 0
      package.json
  4. 82 4
      pnpm-lock.yaml
  5. 9 0
      src/api/system.ts
  6. 342 139
      src/utils/Intercom.ts
  7. 14 5
      src/views/preview-list/children/preview-info.vue

+ 2 - 0
.env.development

@@ -1 +1,3 @@
 VITE_API_BASE_URL= 'https://36.133.244.231:15000'
+VITE_API_INTERCOM_TOKEN_IP ='36.133.244.231:667'
+VITE_API_INTERCOM_IP = '36.133.244.231:666'

+ 3 - 0
.env.production

@@ -1 +1,4 @@
 VITE_API_BASE_URL= '/'
+VITE_API_INTERCOM_TOKEN_IP ='36.133.244.231:667'
+VITE_API_INTERCOM_IP = '36.133.244.231:666'
+

+ 2 - 0
package.json

@@ -32,12 +32,14 @@
   "dependencies": {
     "@ffmpeg/ffmpeg": "^0.12.10",
     "@ffmpeg/util": "^0.12.1",
+    "@fingerprintjs/fingerprintjs": "^4.5.1",
     "@vueuse/core": "^9.3.0",
     "animate.css": "^4.1.1",
     "axios": "^0.24.0",
     "dayjs": "^1.11.5",
     "echarts": "^5.4.0",
     "echarts-liquidfill": "^3.1.0",
+    "livekit-client": "^2.7.5",
     "lodash": "^4.17.21",
     "mitt": "^3.0.0",
     "mp4box": "^0.5.2",

+ 82 - 4
pnpm-lock.yaml

@@ -16,6 +16,9 @@ dependencies:
   '@ffmpeg/util':
     specifier: ^0.12.1
     version: 0.12.1
+  '@fingerprintjs/fingerprintjs':
+    specifier: ^4.5.1
+    version: 4.5.1
   '@vueuse/core':
     specifier: ^9.3.0
     version: 9.13.0(vue@3.2.47)
@@ -34,6 +37,9 @@ dependencies:
   echarts-liquidfill:
     specifier: ^3.1.0
     version: 3.1.0(echarts@5.4.2)
+  livekit-client:
+    specifier: ^2.7.5
+    version: 2.7.5
   lodash:
     specifier: ^4.17.21
     version: 4.17.21
@@ -706,6 +712,10 @@ packages:
       '@babel/helper-validator-identifier': 7.24.7
       to-fast-properties: 2.0.0
 
+  /@bufbuild/protobuf@1.10.0:
+    resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==}
+    dev: false
+
   /@bufbuild/protobuf@2.1.0:
     resolution: {integrity: sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==}
     dev: true
@@ -975,6 +985,12 @@ packages:
     engines: {node: '>=18.x'}
     dev: false
 
+  /@fingerprintjs/fingerprintjs@4.5.1:
+    resolution: {integrity: sha512-hKJaRoLHNeUUPhb+Md3pTlY/Js2YR4aXjroaDHpxrjoM8kGnEFyZVZxXo6l3gRyKnQN52Uoqsycd3M73eCdMzw==}
+    dependencies:
+      tslib: 2.7.0
+    dev: false
+
   /@humanwhocodes/config-array@0.11.8:
     resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
     engines: {node: '>=10.10.0'}
@@ -1108,6 +1124,16 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.15
     dev: true
 
+  /@livekit/mutex@1.0.0:
+    resolution: {integrity: sha512-aiUhoThBNF9UyGTxEURFzJLhhPLIVTnQiEVMjRhPnfHNKLfo2JY9xovHKIus7B78UD5hsP6DlgpmAsjrz4U0Iw==}
+    dev: false
+
+  /@livekit/protocol@1.29.4:
+    resolution: {integrity: sha512-dsqxvABHilrMA0BU5m1w8cMWSVeDjV2ZUIUDClNQZju3c30DLMfEYDHU5nmXDfaaHjNIgoRbYR7upJMozG8JJg==}
+    dependencies:
+      '@bufbuild/protobuf': 1.10.0
+    dev: false
+
   /@mrmlnc/readdir-enhanced@2.2.1:
     resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
     engines: {node: '>=4'}
@@ -1155,7 +1181,7 @@ packages:
       open: 8.4.2
       picocolors: 1.1.0
       tiny-glob: 0.2.9
-      tslib: 2.5.0
+      tslib: 2.7.0
     dev: true
 
   /@rollup/pluginutils@4.2.1:
@@ -4092,6 +4118,11 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /events@3.3.0:
+    resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+    engines: {node: '>=0.8.x'}
+    dev: false
+
   /exec-buffer@3.2.0:
     resolution: {integrity: sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==}
     engines: {node: '>=4'}
@@ -5959,6 +5990,20 @@ packages:
       wrap-ansi: 7.0.0
     dev: true
 
+  /livekit-client@2.7.5:
+    resolution: {integrity: sha512-sPhHYwXvG75y1LDC50dDC9k6Z49L2vc/HcMRhzhi7yBca6ofPEebpB0bmPOry4ovrnFA+a8TL1pFR2mko1/clw==}
+    dependencies:
+      '@livekit/mutex': 1.0.0
+      '@livekit/protocol': 1.29.4
+      events: 3.3.0
+      loglevel: 1.9.2
+      sdp-transform: 2.15.0
+      ts-debounce: 4.0.0
+      tslib: 2.7.0
+      typed-emitter: 2.1.0
+      webrtc-adapter: 9.0.1
+    dev: false
+
   /load-json-file@1.1.0:
     resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==}
     engines: {node: '>=0.10.0'}
@@ -6079,6 +6124,11 @@ packages:
       squeak: 1.3.0
     dev: true
 
+  /loglevel@1.9.2:
+    resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
+    engines: {node: '>= 0.6.0'}
+    dev: false
+
   /longest-streak@2.0.4:
     resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==}
     dev: true
@@ -7769,8 +7819,7 @@ packages:
   /rxjs@7.8.0:
     resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
     dependencies:
-      tslib: 2.5.0
-    dev: true
+      tslib: 2.7.0
 
   /safe-array-concat@1.1.2:
     resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
@@ -8047,6 +8096,15 @@ packages:
       compute-scroll-into-view: 1.0.20
     dev: true
 
+  /sdp-transform@2.15.0:
+    resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
+    hasBin: true
+    dev: false
+
+  /sdp@3.2.0:
+    resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
+    dev: false
+
   /seek-bzip@1.0.6:
     resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==}
     hasBin: true
@@ -8841,7 +8899,7 @@ packages:
     engines: {node: ^14.18.0 || >=16.0.0}
     dependencies:
       '@pkgr/utils': 2.3.1
-      tslib: 2.5.0
+      tslib: 2.7.0
     dev: true
 
   /table@5.4.6:
@@ -9002,6 +9060,10 @@ packages:
     resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==}
     dev: true
 
+  /ts-debounce@4.0.0:
+    resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
+    dev: false
+
   /ts-node@10.9.1(@types/node@18.15.11)(typescript@4.9.5):
     resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
     hasBin: true
@@ -9054,6 +9116,9 @@ packages:
     resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
     dev: true
 
+  /tslib@2.7.0:
+    resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
+
   /tsutils@3.21.0(typescript@4.9.5):
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
     engines: {node: '>= 6'}
@@ -9151,6 +9216,12 @@ packages:
       possible-typed-array-names: 1.0.0
     dev: true
 
+  /typed-emitter@2.1.0:
+    resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
+    optionalDependencies:
+      rxjs: 7.8.0
+    dev: false
+
   /typescript@4.9.5:
     resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
     engines: {node: '>=4.2.0'}
@@ -9621,6 +9692,13 @@ packages:
     resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
     dev: true
 
+  /webrtc-adapter@9.0.1:
+    resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==}
+    engines: {node: '>=6.0.0', npm: '>=3.10.0'}
+    dependencies:
+      sdp: 3.2.0
+    dev: false
+
   /which-boxed-primitive@1.0.2:
     resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
     dependencies:

+ 9 - 0
src/api/system.ts

@@ -49,4 +49,13 @@ export function getAlarmImgFiles(path: string, headers = {}): Promise<any> {
     headers,
   });
 }
+export function getToken(params = {}, headers = {}): Promise<any> {
+  return axios.get(
+    `https://${import.meta.env.VITE_API_INTERCOM_TOKEN_IP}/getToken`,
+    {
+      params,
+      headers,
+    }
+  );
+}
 export default {};

+ 342 - 139
src/utils/Intercom.ts

@@ -1,163 +1,366 @@
 import type { Ref } from 'vue';
-import { Message as $msg } from '@arco-design/web-vue';
+// import { Message as $msg } from '@arco-design/web-vue';
+import { getToken } from '@/api/system';
+import E2EEWorker from 'livekit-client/e2ee-worker?worker';
+import type {
+  // ChatMessage,
+  RoomConnectOptions,
+  RoomOptions,
+  // ScalabilityMode,
+  // SimulationScenario,
+  // VideoCaptureOptions,
+  // VideoCodec,
+} from 'livekit-client';
+import {
+  ConnectionQuality,
+  ConnectionState,
+  DisconnectReason,
+  ExternalE2EEKeyProvider,
+  LocalAudioTrack,
+  // LocalParticipant,
+  LogLevel,
+  MediaDeviceFailure,
+  Participant,
+  ParticipantEvent,
+  RemoteParticipant,
+  // RemoteTrackPublication,
+  // RemoteVideoTrack,
+  Room,
+  RoomEvent,
+  ScreenSharePresets,
+  // Track,
+  TrackPublication,
+  // VideoPresets,
+  // VideoQuality,
+  // createAudioAnalyser,
+  setLogLevel,
+  // supportsAV1,
+  // supportsVP9,
+} from 'livekit-client';
+import FingerprintJS from '@fingerprintjs/fingerprintjs';
 
-// 获取摄像头返回的MediaStream
-let localStream: MediaStream;
+// 指纹
+const getFingerprint = async () => {
+  const fpPromise = FingerprintJS.load();
+  const fp = await fpPromise;
+  const result = await fp.get();
+  // const components = {
+  //   languages: result.components.languages,
+  //   colorDepth: result.components.colorDepth,
+  //   deviceMemory: result.components.deviceMemory,
+  //   hardwareConcurrency: result.components.hardwareConcurrency,
+  //   timezone: result.components.timezone,
+  //   platform: result.components.platform,
+  //   vendor: result.components.vendor,
+  //   vendorFlavors: result.components.vendorFlavors,
+  //   cookiesEnabled: result.components.cookiesEnabled,
+  //   colorGamut: result.components.colorGamut,
+  //   hdr: result.components.hdr,
+  //   videoCard: result.components.videoCard,
+  // };
+  return result.visitorId;
+};
 
-let iceType: any;
+const state = {
+  isFrontFacing: false,
+  encoder: new TextEncoder(),
+  decoder: new TextDecoder(),
+  defaultDevices: new Map<MediaDeviceKind, string>(),
+  bitrateInterval: undefined as any,
+  e2eeKeyProvider: new ExternalE2EEKeyProvider(),
+};
 
-let ws: WebSocket;
+let currentRoom: Room | undefined;
 
-const audio = document.createElement('audio');
-audio.autoplay = true;
-audio.style.display = 'none';
-document.body.appendChild(audio);
+let startTime: number;
 
-// 推流用的MediaStream
-const pc: RTCPeerConnection = new RTCPeerConnection({
-  iceServers: [
-    {
-      urls: ['stun:39.107.83.190:3478'],
-    },
-    {
-      urls: ['turn:39.107.83.190:3478'], // 你的TURN服务器地址和端口
-      username: 'test', // 如果需要,填入你的TURN用户名
-      credential: 'password', // 如果需要,填入你的TURN密码
-    },
-  ],
-});
-pc.addEventListener('icecandidate', (event) => {
-  if (event.candidate) {
-    ws.send(
-      JSON.stringify({
-        type: iceType,
-        candidate: event.candidate,
-      })
-    );
+function appendLog(...args: any[]) {
+  let logger = '';
+  for (let i = 0; i < arguments.length; i += 1) {
+    if (typeof args[i] === 'object') {
+      logger += `${
+        JSON && JSON.stringify ? JSON.stringify(args[i], undefined, 2) : args[i]
+      } `;
+    } else {
+      logger += `${args[i]} `;
+    }
   }
-});
-
-pc.addEventListener('track', (event) => {
-  // eslint-disable-next-line prefer-destructuring
-  audio.srcObject = event.streams[0];
-});
-/**
- * 获取摄像头
- */
-function getDevice() {
-  return new Promise((resolve, reject) => {
-    navigator.mediaDevices
-      .getUserMedia({ audio: true, video: false })
-      .then((mediaStream: MediaStream) => {
-        localStream = mediaStream;
-        // localVideo.srcObject = mediaStream;
-        resolve(mediaStream);
-      })
-      .catch((err) => {
-        reject(err);
-      });
-  });
+  // eslint-disable-next-line no-console
+  console.log(logger);
 }
 
-function sendOffer() {
-  iceType = 'offer_ice';
-  // pc.addTrack(localStream.getVideoTracks()[0], localStream);
-  pc.addTrack(localStream.getAudioTracks()[0], localStream);
-  pc.createOffer({
-    offerToReceiveAudio: true,
-    offerToReceiveVideo: false,
-  }).then((offer) => {
-    pc.setLocalDescription(offer).then(() => {
-      ws.send(JSON.stringify(offer));
+function participantConnected(participant: Participant) {
+  appendLog(
+    'participant',
+    participant.identity,
+    'connected',
+    participant.metadata
+  );
+  // eslint-disable-next-line no-console
+  console.log('tracks', participant.trackPublications);
+  participant
+    .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
+      appendLog('track was muted', pub.trackSid, participant.identity);
+    })
+    .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
+      appendLog('track was unmuted', pub.trackSid, participant.identity);
+    })
+    .on(ParticipantEvent.IsSpeakingChanged, () => {
+      // eslint-disable-next-line no-console
+      console.log(participant);
+    })
+    .on(ParticipantEvent.ConnectionQualityChanged, () => {
+      // eslint-disable-next-line no-console
+      console.log(participant);
     });
-  });
 }
 
-function recvOffer(offer: any) {
-  iceType = 'answer_ice';
-  // pc.addTrack(localStream.getVideoTracks()[0], localStream);
-  pc.addTrack(localStream.getAudioTracks()[0], localStream);
-  pc.setRemoteDescription(offer).then(() => {
-    pc.createAnswer().then((answer) => {
-      pc.setLocalDescription(answer).then(() => {
-        ws.send(JSON.stringify(answer));
-      });
-    });
-  });
+function participantDisconnected(participant: RemoteParticipant) {
+  appendLog('participant', participant.sid, 'disconnected');
+
+  // eslint-disable-next-line no-console
+  console.log(participant);
 }
 
-function recvAnswer(answer: any) {
-  pc.setRemoteDescription(answer);
+function handleRoomDisconnect(reason?: DisconnectReason) {
+  if (!currentRoom) return;
+  appendLog('disconnected from room', { reason });
+  currentRoom = undefined;
+  (window as { [k: string]: any }).currentRoom = undefined;
 }
 
-export const IntercomFn = (
-  disabled: Ref<boolean>,
-  host: string = window.location.host
-) => {
-  if (disabled.value) {
-    ws.close();
-    pc.close();
-    return;
-  }
-  ws = new WebSocket(`wss://${host}/webrtc`);
-  // websocket 连接成功消息
-  ws.addEventListener('open', () => {
-    // 向服务端发送连接消息
-    ws.send(
-      JSON.stringify({
-        type: 'connect',
-      })
-    );
-  });
+const connectToRoom = async (
+  url: string,
+  token: string,
+  roomOptions?: RoomOptions,
+  connectOptions?: RoomConnectOptions,
+  shouldPublish?: boolean
+): Promise<Room | undefined> => {
+  const room = new Room(roomOptions);
+
+  startTime = Date.now();
+  await room.prepareConnection(url, token);
+  const prewarmTime = Date.now() - startTime;
+  appendLog(`prewarmed connection in ${prewarmTime}ms`);
+
+  room
+    .on(RoomEvent.ParticipantConnected, participantConnected)
+    .on(RoomEvent.ParticipantDisconnected, participantDisconnected)
+    .on(RoomEvent.Disconnected, handleRoomDisconnect)
+    .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
+    .on(RoomEvent.Reconnected, async () => {
+      appendLog(
+        'Successfully reconnected. server',
+        await room.engine.getConnectedServerAddress()
+      );
+    })
+    .on(RoomEvent.LocalTrackPublished, (pub) => {
+      const track = pub.track as LocalAudioTrack;
 
-  // 收到服务端消息
-  ws.addEventListener('message', (event: MessageEvent) => {
-    const msg = JSON.parse(event.data);
-    switch (msg.type) {
-      case 'connect':
-        if (msg.code === 200) {
-          $msg?.success('连接成功,等待其他用户');
-          disabled.value = true;
-        } else {
-          $msg?.error('连接失败,已经满员');
-        }
-        break;
-
-      case 'create_offer':
-        sendOffer();
-        break;
-
-      case 'offer':
-        recvOffer(msg);
-        break;
-
-      case 'answer':
-        recvAnswer(msg);
-        break;
-
-      case 'offer_ice':
-      case 'answer_ice':
-        pc.addIceCandidate(msg.candidate);
-        break;
-
-      default:
-        break;
+      if (track instanceof LocalAudioTrack) {
+        // const { calculateVolume } = createAudioAnalyser(track);
+        // setInterval(() => {
+        //   $('local-volume')?.setAttribute(
+        //     'value',
+        //     calculateVolume().toFixed(4)
+        //   );
+        // }, 200);
+      }
+    })
+    .on(RoomEvent.RoomMetadataChanged, (metadata) => {
+      appendLog('new metadata for room', metadata);
+    })
+    .on(RoomEvent.MediaDevicesError, (e: Error) => {
+      const failure = MediaDeviceFailure.getFailure(e);
+      appendLog('media device failure', failure);
+    })
+    .on(
+      RoomEvent.ConnectionQualityChanged,
+      (quality: ConnectionQuality, participant?: Participant) => {
+        appendLog('connection quality changed', participant?.identity, quality);
+      }
+    )
+    .on(RoomEvent.TrackSubscribed, (track, pub, participant) => {
+      appendLog('subscribed to track', pub.trackSid, participant.identity);
+    })
+    .on(RoomEvent.TrackUnsubscribed, (_, pub) => {
+      appendLog('unsubscribed from track', pub.trackSid);
+    })
+    .on(RoomEvent.SignalConnected, async () => {
+      const signalConnectionTime = Date.now() - startTime;
+      appendLog(`signal connection established in ${signalConnectionTime}ms`);
+      // speed up publishing by starting to publish before it's fully connected
+      // publishing is accepted as soon as signal connection has established
+      if (shouldPublish) {
+        await room.localParticipant.enableCameraAndMicrophone();
+        appendLog(`tracks published in ${Date.now() - startTime}ms`);
+      }
+    })
+    .on(RoomEvent.TrackStreamStateChanged, (pub, streamState, participant) => {
+      appendLog(
+        `stream state changed for ${pub.trackSid} (${
+          participant.identity
+        }) to ${streamState.toString()}`
+      );
+    });
+
+  try {
+    // read and set current key from input
+    const cryptoKey = '';
+    const e2eChack = false;
+    state.e2eeKeyProvider.setKey(cryptoKey);
+    if (e2eChack) {
+      await room.setE2EEEnabled(true);
     }
-  });
 
-  ws.addEventListener('close', () => {
-    $msg?.warning('websocket 连接断开');
-    disabled.value = false;
+    await room.connect(url, token, connectOptions);
+    const elapsed = Date.now() - startTime;
+    appendLog(
+      `successfully connected to ${room.name} in ${Math.round(elapsed)}ms`,
+      await room.engine.getConnectedServerAddress()
+    );
+  } catch (error: any) {
+    let message: any = error;
+    if (error.message) {
+      message = error.message;
+    }
+    appendLog('could not connect:', message);
+    return undefined;
+  }
+  currentRoom = room;
+  (window as { [k: string]: any }).currentRoom = room;
+
+  room.remoteParticipants.forEach((participant) => {
+    participantConnected(participant);
   });
+  participantConnected(room.localParticipant);
+
+  return room;
 };
-getDevice()
-  .then(() => {
-    // 获取摄像头成功
-  })
-  .catch((err) => {
-    $msg?.error('获取摄像头失败');
+
+function renderBitrate() {
+  if (!currentRoom || currentRoom.state !== ConnectionState.Connected) {
+    return;
+  }
+  const participants: Participant[] = [
+    ...currentRoom.remoteParticipants.values(),
+  ];
+  participants.push(currentRoom.localParticipant);
+
+  // eslint-disable-next-line no-restricted-syntax
+  for (const p of participants) {
+    let totalBitrate = 0;
+    // eslint-disable-next-line no-restricted-syntax
+    for (const t of p.trackPublications.values()) {
+      if (t.track) {
+        totalBitrate += t.track.currentBitrate;
+      }
+
+      // if (t.source === Track.Source.Camera) {
+      //   if (t.videoTrack instanceof RemoteVideoTrack) {
+      //     // eslint-disable-next-line no-console
+      //     console.log(t.videoTrack.getDecoderImplementation() ?? '');
+      //   }
+      // }
+    }
+    let displayText = '';
+    if (totalBitrate > 0) {
+      displayText = `${Math.round(totalBitrate / 1024).toLocaleString()} kbps`;
+    }
     // eslint-disable-next-line no-console
-    console.error(err);
-  });
+    console.log(displayText);
+  }
+}
+
+const connectWithFormInput = async (
+  url: any,
+  token: any,
+  disabled: Ref<string>
+) => {
+  const simulcast = true; // 同步播出
+  const dynacast = true; // Dynacast
+  const forceTURN = false; // 强制转向
+  const adaptiveStream = true; // 自适应流
+  const shouldPublish = true; // 发布
+  // const preferredCodec = '' as VideoCodec;
+  // const scalabilityMode = '';
+  // const cryptoKey = '';
+  const autoSubscribe = true; // 自动订阅
+  const e2eeEnabled = false;
+  const audioOutputId = ''; // E2EE密钥
+  setLogLevel(LogLevel.debug);
+
+  const roomOpts: RoomOptions = {
+    adaptiveStream,
+    dynacast,
+    audioOutput: {
+      deviceId: audioOutputId,
+    },
+    publishDefaults: {
+      simulcast,
+      // videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216],
+      // videoCodec: preferredCodec || 'vp8',
+      dtx: true,
+      red: true,
+      forceStereo: false,
+      screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
+      scalabilityMode: 'L3T3_KEY',
+    },
+    // videoCaptureDefaults: {
+    //   resolution: VideoPresets.h720.resolution,
+    // },
+    e2ee: e2eeEnabled
+      ? { keyProvider: state.e2eeKeyProvider, worker: new E2EEWorker() }
+      : undefined,
+  };
+  // if (
+  //   roomOpts.publishDefaults?.videoCodec === 'av1' ||
+  //   roomOpts.publishDefaults?.videoCodec === 'vp9'
+  // ) {
+  //   roomOpts.publishDefaults.backupCodec = true;
+  //   if (scalabilityMode !== '') {
+  //     roomOpts.publishDefaults.scalabilityMode =
+  //       scalabilityMode as ScalabilityMode;
+  //   }
+  // }
+
+  const connectOpts: RoomConnectOptions = {
+    autoSubscribe,
+  };
+  if (forceTURN) {
+    connectOpts.rtcConfig = {
+      iceTransportPolicy: 'relay',
+    };
+  }
+  await connectToRoom(url, token, roomOpts, connectOpts, shouldPublish);
+  disabled.value = 'open';
+
+  state.bitrateInterval = setInterval(renderBitrate, 1000);
+};
+
+const disconnectRoom = (disabled: Ref<string>) => {
+  if (currentRoom) {
+    currentRoom.disconnect();
+  }
+  if (state.bitrateInterval) {
+    clearInterval(state.bitrateInterval);
+  }
+  disabled.value = 'close';
+};
+
+export const IntercomFn = async (disabled: Ref<string>, guid: string) => {
+  if (disabled.value === 'open') {
+    disconnectRoom(disabled);
+    return;
+  }
+  disabled.value = 'loading';
+  const fingerprint = await getFingerprint();
+  const token = await getToken({ Room: guid, Identity: fingerprint });
+  connectWithFormInput(
+    `wss://${import.meta.env.VITE_API_INTERCOM_IP}`,
+    token,
+    disabled
+  );
+};
 
 export default null;

+ 14 - 5
src/views/preview-list/children/preview-info.vue

@@ -68,14 +68,23 @@
               </a-popover>
               <!-- 对讲 -->
               <a-tooltip :content="$t('previewList.dj')">
-                <a-button @click="intercomClick">
+                <a-button
+                  :disabled="intercomDisabled === 'loading'"
+                  @click="intercomClick"
+                >
                   <template #icon>
                     <Icon
-                      v-if="!intercomDisabled"
+                      v-if="intercomDisabled === 'close'"
                       icon="material-symbols:record-voice-over-outline"
                       width="22"
                       height="22"
                     />
+                    <Icon
+                      v-else-if="intercomDisabled === 'loading'"
+                      icon="eos-icons:three-dots-loading"
+                      width="22"
+                      height="22"
+                    />
                     <Icon
                       v-else
                       class="fade-open"
@@ -532,16 +541,16 @@
     // toggle();
   };
 
-  const intercomDisabled = ref(false);
+  const intercomDisabled = ref<'open' | 'loading' | 'close'>('close');
   const intercomClick = () => {
     // (app.ip as string).split('//')[1]
-    IntercomFn(intercomDisabled, '39.107.83.190');
+    IntercomFn(intercomDisabled, props.data.id);
   };
   const viewDrawerRef = ref();
   const showView = (record: any) => {
     record.isRead = true;
     console.log(record);
-    
+
     viewData.value = record;
     viewDrawerRef.value.open();
   };