| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 è¿æ¥ / è®¾å¤ / äºä»¶çèå°è£
ï¼UI ç»ä»¶åªå
³å¿ååºå¼ç¶æ */ |
| | | 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 深度代ç SDK class å
é¨ */ |
| | | const remoteParticipants = shallowRef<RemoteParticipant[]>([]) |
| | | /** è¿æ¥ç¶æ */ |
| | | const isConnected = ref(false) |
| | | /** è¿æ¥è´¨é */ |
| | | const connectionQuality = ref<ConnectionQuality>(ConnectionQuality.Unknown) |
| | | /** 麦å
é£å¼å
³ */ |
| | | const micEnabled = ref(true) |
| | | /** æå头å¼å
³ */ |
| | | const cameraEnabled = ref(false) |
| | | /** æ¬å£°å¨å¼å
³ï¼æµè§å¨æ ç³»ç»çº§ 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ï¼audio / 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 çè·¯éç®ï¼video å
ç´ è½è§£ç» 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 ç¼åï¼key 为 `${participantSid}:${source}`ï¼value 为 `{ track, stream }`ï¼ |
| | | * å䏿¡ MediaStreamTrack å¤ç¨åä¸ä¸ª MediaStreamï¼é¿å
<video>.srcObject åå¤éæå¯¼è´è§£ç 管线é建ï¼è§é¢éªçï¼ï¼ |
| | | * track å¼ç¨åæ¢ï¼éæ°è®¢é
/ åæµï¼æ¶æéæ°å»ºå¹¶æ¿æ¢ |
| | | */ |
| | | const streamCache = new Map<string, { stream: MediaStream; track: MediaStreamTrack; }>() |
| | | |
| | | /** |
| | | * ååä¸è
æå®æ¥æºç轨éå¹¶æå
为 MediaStreamï¼ |
| | | * åä¸è
èµ° unknown æ¯å 为ååºå¼ç³»ç»ä¼ä¸¢å¤± livekit-client çç±»ç¾åï¼å½æ°å
æå¨ cast ååä¸è
ç±»åï¼ |
| | | * å½ä¸ç¼åè¿ååä¸ MediaStream å¼ç¨ï¼ä¸æ¸¸ watch / srcObject æ ééæï¼ |
| | | * 轨é被 muteï¼æ¬ç«¯å
³æå头 / è¿ç«¯å
³æåå¤´ï¼æ¶è¿å nullï¼è®© video å
ç´ è§£ç» 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ï¼clearHandlers 为 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> |