import { Room, RoomEvent, createLocalTracks, createLocalAudioTrack, createLocalVideoTrack, } from 'livekit-client' console.log("Room 构造函数:", Room) class Trtc { constructor() { this.room = null this.localTracks = [] } // 安全 attach 远端轨道 attachTrack(track, participant) { const container = document.getElementById('video-' + participant.identity) if (!container) return // 已经 attach 过就跳过,避免闪屏 if (track.attachedElements.length > 0) return const element = track.attach() element.style.width = '100%' element.style.height = '100%' element.style.objectFit = 'cover' container.appendChild(element) } async joinRoom(token, url, userId, onRemoteTrack) { try { // 如果已有房间,先退出 if (this.room) { await this.leaveRoom() } this.room = new Room() // 监听远端轨道 this.room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => { console.log('📡 TrackSubscribed:', track.kind, participant.identity) if (track.kind === 'video' || track.kind === 'audio') { this.attachTrack(track, participant) if (typeof onRemoteTrack === 'function') { onRemoteTrack(track, participant) } } }) // 检查设备 this.localTracks = [] if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { const devices = await navigator.mediaDevices.enumerateDevices() const hasMic = devices.some(d => d.kind === 'audioinput') const hasCam = devices.some(d => d.kind === 'videoinput') if (hasMic || hasCam) { this.localTracks = await createLocalTracks({ audio: hasMic, video: hasCam, }) console.log('本地轨道列表:', this.localTracks) } else { console.warn('未检测到摄像头或麦克风,将以静默方式加入房间') } } else { console.warn('当前环境不支持 mediaDevices,将以纯接收模式加入房间') } console.log('发起连接到 LiveKit ...', url) // 监听连接状态 const connectedPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('连接 LiveKit 超时')) }, 15000) try { this.room.once(RoomEvent.Connected, () => { clearTimeout(timeout) console.log('成功连接到 LiveKit 房间') resolve() // 主动挂载已有轨道 if (this.room.participants && typeof this.room.participants.values === 'function') { for (const participant of this.room.participants.values()) { participant.tracks.forEach(publication => { if (publication.isSubscribed && publication.track) { this.attachTrack(publication.track, participant) if (typeof onRemoteTrack === 'function') { onRemoteTrack(publication.track, participant) } } }) } } }) this.room.once(RoomEvent.ConnectionError, (err) => { clearTimeout(timeout) console.error('LiveKit 连接错误:', err) reject(err) }) } catch (e) { console.error('LiveKit连接失败:', e) } }) this.room.on(RoomEvent.Disconnected, () => { console.warn('与 LiveKit 断开连接') }) // 发起连接 await this.room.connect(url, token, { autoSubscribe: true, }) await connectedPromise // 发布本地轨道 for (const track of this.localTracks) { console.log('正在发布轨道:', track.kind) await this.room.localParticipant.publishTrack(track) } return this.room } catch (err) { console.error('joinRoom 出错:', err) throw err } } getLocalVideoTrack() { return this.localTracks.find(t => t.kind === 'video') || null } async leaveRoom() { if (this.room) { console.log("Trtc 离开房间") // 停止并释放本地轨道 this.localTracks.forEach(track => { try { track.stop() track.detach() } catch (e) { console.warn("轨道释放失败:", e) } }) try { await this.room.disconnect() } catch (e) { console.error("房间断开异常:", e) } this.room = null this.localTracks = [] } else { this.room = null console.warn("离开房间 时 room 为 null") } } } export { createLocalAudioTrack, createLocalVideoTrack, } export default new Trtc()