From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块

---
 src/views/im/home/composables/useLiveKitRoom.ts |  331 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 331 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/composables/useLiveKitRoom.ts b/src/views/im/home/composables/useLiveKitRoom.ts
new file mode 100644
index 0000000..7cc0a5a
--- /dev/null
+++ b/src/views/im/home/composables/useLiveKitRoom.ts
@@ -0,0 +1,331 @@
+import { computed, ref, shallowRef } from 'vue'
+
+import {
+  ConnectionQuality,
+  type LocalParticipant,
+  type Participant,
+  type RemoteParticipant,
+  Room,
+  RoomEvent,
+  Track,
+  VideoPresets
+} from 'livekit-client'
+
+type ParticipantEventHandler = (userId: number) => void
+
+/** LiveKit Room 杩炴帴 / 璁惧 / 浜嬩欢鐨勮杽灏佽锛沀I 缁勪欢鍙叧蹇冨搷搴斿紡鐘舵�� */
+export function useLiveKitRoom() {
+  /** Room 瀹炰緥锛涙ā鍧楀唴閮ㄧ姸鎬侊紝涓嶅澶栨毚闇诧紝閬垮厤璋冪敤鏂硅鍐� */
+  const _room = shallowRef<null | Room>(null)
+  /** 鍙 room 寮曠敤锛涜皟鐢ㄦ柟浠呯敤浜庡箓绛夊垽瀹� */
+  const room = computed(() => _room.value)
+  /** 鏈湴鍙備笌鑰咃紱杩炴帴鎴愬姛鍚庤祴鍊� */
+  const localParticipant = shallowRef<LocalParticipant | null>(null)
+  /** 杩滅鍙備笌鑰呭垪琛紱ParticipantConnected / Disconnected 鏃跺埛鏂帮紱shallowRef 閬垮厤 Vue 娣卞害浠g悊 SDK class 鍐呴儴 */
+  const remoteParticipants = shallowRef<RemoteParticipant[]>([])
+  /** 杩炴帴鐘舵�� */
+  const isConnected = ref(false)
+  /** 杩炴帴璐ㄩ噺 */
+  const connectionQuality = ref<ConnectionQuality>(ConnectionQuality.Unknown)
+  /** 楹﹀厠椋庡紑鍏� */
+  const micEnabled = ref(true)
+  /** 鎽勫儚澶村紑鍏� */
+  const cameraEnabled = ref(false)
+  /** 鎵0鍣ㄥ紑鍏筹紱娴忚鍣ㄦ棤绯荤粺绾� API锛岄�氳繃 audio 鍏冪礌 muted 灞炴�у疄鐜拌繙绔煶棰戦潤闊� */
+  const speakerEnabled = ref(true)
+  /** 灞忓箷鍏变韩寮�鍏� */
+  const screenShareEnabled = ref(false)
+  /** 褰撳墠鏄惁澶勪簬銆岄噸杩炰腑銆嶏紱鐬柇鏃� UI 鏄剧ず鎻愮ず鑰屼笉寮哄埗缁撴潫閫氳瘽 */
+  const reconnecting = ref(false)
+  /** 杩滅鏂紑璁㈤槄鑰咃紱閫氳瘽缁撴潫鏃剁粺涓�娓呯┖ */
+  const disconnectedHandlers = new Set<() => void>()
+  /** 鎴垮唴鏌愪汉鍔犲叆璁㈤槄鑰咃紱涓诲彨绔敤浜庝粠 INVITING 鍒囧埌 RUNNING */
+  const participantConnectedHandlers = new Set<ParticipantEventHandler>()
+  /** 鎴垮唴鏌愪汉绂诲紑璁㈤槄鑰咃紱鐢ㄤ簬鎶� userId 鏍囪涓恒�屽凡閫�鍑恒�嶄粠 pending 鍗犱綅涓Щ闄� */
+  const participantDisconnectedHandlers = new Set<ParticipantEventHandler>()
+
+  /** 鍚屾杩滅鍙備笌鑰呭垪琛ㄥ埌鍝嶅簲寮忔暟缁� */
+  function syncRemotes(r: Room) {
+    remoteParticipants.value = [...r.remoteParticipants.values()]
+  }
+
+  /** 杩炴帴 LiveKit Server锛沘udio / video 鎺у埗鍒濆榛樿寮�鍏� */
+  async function connect(url: string, token: string, opts: { audio?: boolean; video?: boolean }) {
+    // 鏂拌繛鎺ュ墠鍏堟柇寮�鏃� Room锛涗繚鐣欐湰娆℃敞鍐岀殑浜嬩欢鍥炶皟
+    if (_room.value) {
+      await disconnectRoom(false)
+    }
+    const r = new Room({
+      // 鎸夋牸瀛愬昂瀵歌嚜鍔ㄩ�� simulcast 灞�
+      adaptiveStream: true,
+      // 鏈闃呯殑灞傚姩鎬佸仠鍙戯紝鑺傜渷涓婅
+      dynacast: true,
+      // 閲囬泦鍒嗚鲸鐜� 720p锛岀‘淇濆ぇ鏍煎瓙娓呮櫚
+      videoCaptureDefaults: {
+        resolution: VideoPresets.h720.resolution
+      },
+      // 鍙戝竷缂栫爜涓婇檺 1.5 Mbps / 30fps锛涗繚鐣欓粯璁� simulcast 涓夊眰锛�180p / 360p / 720p锛�
+      publishDefaults: {
+        videoEncoding: {
+          maxBitrate: 1_500_000,
+          maxFramerate: 30,
+          priority: 'high'
+        },
+        // 灞忓箷鍏变韩鐮佺巼 3 Mbps锛屾枃瀛楃晫闈㈡竻鏅�
+        screenShareEncoding: {
+          maxBitrate: 3_000_000,
+          maxFramerate: 15,
+          priority: 'medium'
+        }
+      }
+    })
+    _room.value = r
+
+    r.on(RoomEvent.ParticipantConnected, (rp) => {
+        syncRemotes(r)
+        const userId = parseUserId(rp.identity)
+        if (userId != null) {
+          participantConnectedHandlers.forEach((cb) => cb(userId))
+        }
+      })
+      .on(RoomEvent.ParticipantDisconnected, (rp) => {
+        syncRemotes(r)
+        // 绂诲紑鐨勫弬涓庤�呯紦瀛樻竻鎺夛紝閬垮厤涓嬫鍚� sid 閲嶈繛鍛戒腑澶辨晥寮曠敤
+        for (const key of streamCache.keys()) {
+          if (key.startsWith(`${rp.sid}:`)) {
+            streamCache.delete(key)
+          }
+        }
+        const userId = parseUserId(rp.identity)
+        if (userId != null) {
+          participantDisconnectedHandlers.forEach((cb) => cb(userId))
+        }
+      })
+      .on(RoomEvent.TrackSubscribed, () => syncRemotes(r))
+      .on(RoomEvent.TrackUnsubscribed, () => syncRemotes(r))
+      // mute / unmute 璁� pickStream 鐨� isMuted 鐭矾閲嶇畻锛寁ideo 鍏冪礌鑳借В缁� srcObject 鑰屼笉鏄崱鏈�鍚庝竴甯�
+      .on(RoomEvent.TrackMuted, () => syncRemotes(r))
+      .on(RoomEvent.TrackUnmuted, () => syncRemotes(r))
+      .on(RoomEvent.ConnectionQualityChanged, (quality) => {
+        connectionQuality.value = quality
+      })
+      // 鐬柇 鈫� 鏄剧ず銆岄噸杩炰腑銆嶏紱涓嶅叧閫氳瘽绐楋紝鐢� SDK 鍐呴儴閲嶈繛鏈哄埗鎭㈠
+      .on(RoomEvent.Reconnecting, () => {
+        reconnecting.value = true
+      })
+      .on(RoomEvent.Reconnected, () => {
+        reconnecting.value = false
+      })
+      // 閲嶈繛澶辫触 / 涓诲姩鏂� / 琚涪鏃惰Е鍙戞竻鐞�
+      .on(RoomEvent.Disconnected, () => {
+        isConnected.value = false
+        reconnecting.value = false
+        disconnectedHandlers.forEach((cb) => cb())
+      })
+
+    // 棰勭儹 getUserMedia 涓� WebSocket 鎻℃墜骞惰锛岀渷 100锝�300ms 涓茶寤惰繜锛�
+    // 鎷垮埌鐨� stream 浠呯敤浜庤Е鍙戞潈闄愬脊绐� + 璁惧灏辩华锛屾彙鎵嬪畬鎴愬悗鐢� LiveKit 鍐呴儴閲嶆柊璇锋眰璁惧鍙戝竷杞�
+    const warmup = prewarmMedia(opts)
+    // 寤虹珛 WebSocket 淇′护 + WebRTC 濯掍綋閫氶亾锛涘畬鎴愬悗 localParticipant 鍙敤锛屽凡鍦ㄦ埧鍙備笌鑰呬細閫氳繃 ParticipantConnected 浜嬩欢鎵归噺鎺ㄩ��
+    await r.connect(url, token)
+    // 鏈熼棿琚閮� disconnect 鏇挎崲锛涗腑姝㈠悗缁� publish锛岄伩鍏嶆憚鍍忓ご琚噸鏂板惎鐢�
+    if (_room.value !== r) {
+      return
+    }
+    localParticipant.value = r.localParticipant
+    isConnected.value = true
+
+    // 棰勭儹缁撴灉涓嶇洿鎺ュ彂甯冿紙閬垮厤 SDK 涓庡閮� track 鐢熷懡鍛ㄦ湡绾犵紶锛夛紝浠呯瓑寰呮潈闄愬氨缁悗鍐嶈蛋鏍囧噯 setXxxEnabled
+    await warmup
+    if (_room.value !== r) {
+      return
+    }
+    // 楹﹀厠椋庝笌鎽勫儚澶存潈闄愮浉浜掔嫭绔嬶紝骞惰鍚敤鍙戝竷
+    const inits: Promise<unknown>[] = []
+    if (opts.audio) {
+      inits.push(r.localParticipant.setMicrophoneEnabled(true))
+    }
+    if (opts.video) {
+      inits.push(r.localParticipant.setCameraEnabled(true))
+    }
+    if (inits.length > 0) {
+      await Promise.all(inits)
+    }
+    micEnabled.value = !!opts.audio
+    cameraEnabled.value = !!opts.video
+
+    // 鍏滃簳鍚屾涓�娆¤繙绔垪琛細r.connect 鏈熼棿 ParticipantConnected 浜嬩欢鍙兘鍦� handler 缁戝畾鍓嶈Е鍙戣鍚烇紝瀵艰嚧棣栧睆婕忎汉
+    syncRemotes(r)
+  }
+
+  /** 鎻愬墠瑙﹀彂鏉冮檺寮圭獥 + 璁惧鍞よ捣锛屼覆琛屽欢杩熷湪 r.connect 鏈熼棿涓�璧疯窇锛涘け璐ラ潤榛橈紙杩炴帴鍚庝細鍐嶈瘯涓�娆★級 */
+  async function prewarmMedia(opts: { audio?: boolean; video?: boolean }): Promise<void> {
+    if (!opts.audio && !opts.video) {
+      return
+    }
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({
+        audio: !!opts.audio,
+        video: !!opts.video
+      })
+      // 鎷垮埌鏉冮檺鍗冲彲锛岀珛鍗冲仠鎺夋墍鏈� track 閲婃斁璁惧锛涙寮忓彂甯冭蛋 SDK 娴佺▼閲嶆柊璇锋眰
+      stream.getTracks().forEach((t) => t.stop())
+    } catch {
+      // 鐢ㄦ埛鎷掔粷 / 璁惧鍗犵敤绛夊紓甯革紝浜ょ粰鍚庣画 setXxxEnabled 鍐嶆灏濊瘯鎶ラ敊
+    }
+  }
+
+  /** 鍒囬害鍏嬮 */
+  async function setMicEnabled(enabled: boolean) {
+    if (!_room.value) {
+      return
+    }
+    await _room.value.localParticipant.setMicrophoneEnabled(enabled)
+    micEnabled.value = enabled
+  }
+
+  /** 鍒囨憚鍍忓ご */
+  async function setCameraEnabled(enabled: boolean) {
+    if (!_room.value) {
+      return
+    }
+    await _room.value.localParticipant.setCameraEnabled(enabled)
+    cameraEnabled.value = enabled
+  }
+
+  /** 鍒囨壃澹板櫒锛涗粎鍒囧搷搴斿紡鐘舵�侊紝瀹為檯闈欓煶鐢辨ā鏉夸笂 audio 鍏冪礌 :muted 缁戝畾鐢熸晥 */
+  function setSpeakerEnabled(enabled: boolean) {
+    speakerEnabled.value = enabled
+  }
+
+  /**
+   * 鍒囧睆骞曞叡浜紱
+   *
+   * 娴忚鍣ㄤ細寮瑰師鐢熴�岄�夋嫨鍏变韩鍐呭銆嶅璇濇锛岀敤鎴峰湪寮圭獥閲岀偣鍙栨秷鏃� setScreenShareEnabled 浼氭姏閿欙紝鎹曡幏骞舵妸鐘舵�佸浣嶅洖 SDK 鐨勫疄闄呭��
+   */
+  async function setScreenShareEnabled(enabled: boolean) {
+    if (!_room.value) return
+    try {
+      await _room.value.localParticipant.setScreenShareEnabled(enabled)
+      screenShareEnabled.value = enabled
+    } catch (error) {
+      // 鐢ㄦ埛鍦ㄦ祻瑙堝櫒鍘熺敓瀵硅瘽妗嗛噷鍙栨秷閫夋嫨锛屼笉褰撲綔閿欒
+      screenShareEnabled.value = _room.value.localParticipant.isScreenShareEnabled
+      throw error
+    }
+  }
+
+  /** 娉ㄥ唽銆岃繙绔繛鎺ュ紓甯告柇寮�銆嶅洖璋冿紱杩斿洖鍙嶆敞鍐屽嚱鏁� */
+  function onDisconnected(cb: () => void): () => void {
+    disconnectedHandlers.add(cb)
+    return () => disconnectedHandlers.delete(cb)
+  }
+
+  /** 娉ㄥ唽銆屾埧鍐呮煇浜哄姞鍏ャ�嶅洖璋冿紱杩斿洖鍙嶆敞鍐屽嚱鏁� */
+  function onParticipantConnected(cb: ParticipantEventHandler): () => void {
+    participantConnectedHandlers.add(cb)
+    return () => participantConnectedHandlers.delete(cb)
+  }
+
+  /** 娉ㄥ唽銆屾埧鍐呮煇浜虹寮�銆嶅洖璋冿紱杩斿洖鍙嶆敞鍐屽嚱鏁� */
+  function onParticipantDisconnected(cb: ParticipantEventHandler): () => void {
+    participantDisconnectedHandlers.add(cb)
+    return () => participantDisconnectedHandlers.delete(cb)
+  }
+
+  /** identity 鏄悗绔 token 鏃跺鐨� userId 瀛楃涓诧紝杞� number 杩斿洖锛涢潪鏁板瓧锛堝吋瀹规�у厹搴曪級杩斿洖 null */
+  function parseUserId(identity: string): null | number {
+    const id = Number(identity)
+    return Number.isNaN(id) ? null : id
+  }
+
+  /**
+   * MediaStream 缂撳瓨锛沰ey 涓� `${participantSid}:${source}`锛寁alue 涓� `{ track, stream }`锛�
+   * 鍚屼竴鏉� MediaStreamTrack 澶嶇敤鍚屼竴涓� MediaStream锛岄伩鍏� <video>.srcObject 鍙嶅閲嶆寕瀵艰嚧瑙g爜绠$嚎閲嶅缓锛堣棰戦棯鐑侊級锛�
+   * track 寮曠敤鍒囨崲锛堥噸鏂拌闃� / 鍒囨祦锛夋椂鎸夐渶鏂板缓骞舵浛鎹�
+   */
+  const streamCache = new Map<string, { stream: MediaStream; track: MediaStreamTrack; }>()
+
+  /**
+   * 鍙栧弬涓庤�呮寚瀹氭潵婧愮殑杞ㄩ亾骞舵墦鍖呬负 MediaStream锛�
+   * 鍙備笌鑰呰蛋 unknown 鏄洜涓哄搷搴斿紡绯荤粺浼氫涪澶� livekit-client 鐨勭被绛惧悕锛屽嚱鏁板唴鎵嬪姩 cast 鍥炲弬涓庤�呯被鍨嬶紱
+   * 鍛戒腑缂撳瓨杩斿洖鍚屼竴 MediaStream 寮曠敤锛屼笅娓� watch / srcObject 鏃犻渶閲嶆寕锛�
+   * 杞ㄩ亾琚� mute锛堟湰绔叧鎽勫儚澶� / 杩滅鍏虫憚鍍忓ご锛夋椂杩斿洖 null锛岃 video 鍏冪礌瑙g粦 srcObject 鑰屼笉鏄崱鍦ㄦ渶鍚庝竴甯�
+   */
+  function pickStream(participant: unknown, source: Track.Source): MediaStream | null {
+    const p = participant as Participant
+    const pub = p.getTrackPublication(source)
+    if (!pub || pub.isMuted) {
+      return null
+    }
+    const track = pub.track?.mediaStreamTrack
+    if (!track) {
+      return null
+    }
+    const key = `${p.sid}:${source}`
+    const cached = streamCache.get(key)
+    if (cached && cached.track === track) {
+      return cached.stream
+    }
+    const stream = new MediaStream([track])
+    streamCache.set(key, { track, stream })
+    return stream
+  }
+
+  /** 鏂紑褰撳墠 Room锛沜learHandlers 涓� true 鏃跺悓姝ユ竻鐞嗗閮ㄦ敞鍐岀殑浜嬩欢鍥炶皟 */
+  async function disconnectRoom(clearHandlers: boolean) {
+    // 娓呯悊閫氳瘽缁撴潫鍚庝笉鍐嶅鐢ㄧ殑璁㈤槄鍥炶皟
+    if (clearHandlers) {
+      disconnectedHandlers.clear()
+      participantConnectedHandlers.clear()
+      participantDisconnectedHandlers.clear()
+    }
+    // 娓呯悊闊宠棰戣建閬撶紦瀛�
+    streamCache.clear()
+    if (_room.value) {
+      // 鍗歌浇 Room 浜嬩欢骞舵柇寮�杩炴帴
+      _room.value.removeAllListeners()
+      await _room.value.disconnect()
+      _room.value = null
+    }
+    // 閲嶇疆杩炴帴鍜岃澶囩姸鎬�
+    localParticipant.value = null
+    remoteParticipants.value = []
+    isConnected.value = false
+    reconnecting.value = false
+    micEnabled.value = true
+    cameraEnabled.value = false
+    speakerEnabled.value = true
+    screenShareEnabled.value = false
+  }
+
+  /** 涓诲姩鏂紑锛涢�氳瘽缁撴潫缁熶竴璋� */
+  async function disconnect() {
+    await disconnectRoom(true)
+  }
+
+  return {
+    room,
+    localParticipant,
+    remoteParticipants,
+    isConnected,
+    connectionQuality,
+    micEnabled,
+    cameraEnabled,
+    speakerEnabled,
+    screenShareEnabled,
+    reconnecting,
+    connect,
+    disconnect,
+    setMicEnabled,
+    setCameraEnabled,
+    setSpeakerEnabled,
+    setScreenShareEnabled,
+    pickStream,
+    onDisconnected,
+    onParticipantConnected,
+    onParticipantDisconnected
+  }
+}
+
+export type ImLiveKitRoom = ReturnType<typeof useLiveKitRoom>

--
Gitblit v1.9.3