From 5367b3b4d92588c4e76728e6bd4ad6aae0cbb967 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 23 七月 2026 13:05:25 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
---
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