|
@@ -0,0 +1,158 @@
|
|
|
|
+import {
|
|
|
|
+ Room,
|
|
|
|
+ RoomEvent,
|
|
|
|
+ createLocalTracks,
|
|
|
|
+ createLocalAudioTrack,
|
|
|
|
+ createLocalVideoTrack,
|
|
|
|
+} from 'livekit-client'
|
|
|
|
+
|
|
|
|
+class Trtc {
|
|
|
|
+ constructor() {
|
|
|
|
+ this.room = null
|
|
|
|
+ this.localTracks = []
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ async joinRoom(token, url, userId, onRemoteTrack) {
|
|
|
|
+ try {
|
|
|
|
+ if (this.room) {
|
|
|
|
+ this.leaveRoom()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.room = new Room()
|
|
|
|
+
|
|
|
|
+ // 挂载远端轨道(带 DOM 等待机制)
|
|
|
|
+ const attachRemoteTrack = (track, participant) => {
|
|
|
|
+ const id = 'video-' + participant.identity
|
|
|
|
+ const maxRetry = 20
|
|
|
|
+ let retry = 0
|
|
|
|
+
|
|
|
|
+ const tryAttach = () => {
|
|
|
|
+ const container = document.getElementById(id)
|
|
|
|
+ if (container) {
|
|
|
|
+ console.log(`📺 正在挂载视频到容器: ${id}`)
|
|
|
|
+ container.innerHTML = ''
|
|
|
|
+ const element = track.attach()
|
|
|
|
+ element.style.width = '100%'
|
|
|
|
+ element.style.height = '100%'
|
|
|
|
+ element.style.objectFit = 'cover'
|
|
|
|
+ container.appendChild(element)
|
|
|
|
+ } else if (retry < maxRetry) {
|
|
|
|
+ retry++
|
|
|
|
+ setTimeout(tryAttach, 200)
|
|
|
|
+ } else {
|
|
|
|
+ console.warn(`❌ 超时未找到远端容器: ${id}`)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ tryAttach()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 监听远端轨道
|
|
|
|
+ this.room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
|
|
|
+ console.log('📡 TrackSubscribed:', track.kind, participant.identity)
|
|
|
|
+ if (track.kind === 'video' || track.kind === 'audio') {
|
|
|
|
+ attachRemoteTrack(track, participant)
|
|
|
|
+ if (typeof onRemoteTrack === 'function') {
|
|
|
|
+ onRemoteTrack(track, participant)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 检查设备
|
|
|
|
+ this.localTracks = []
|
|
|
|
+ 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('未检测到摄像头或麦克风,将以静默方式加入房间')
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ console.log('🟢 发起连接到 LiveKit ...', url)
|
|
|
|
+
|
|
|
|
+ // 监听连接状态
|
|
|
|
+ const connectedPromise = new Promise((resolve, reject) => {
|
|
|
|
+ const timeout = setTimeout(() => {
|
|
|
|
+ reject(new Error('连接 LiveKit 超时'))
|
|
|
|
+ }, 15000)
|
|
|
|
+
|
|
|
|
+ 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) {
|
|
|
|
+ console.log('🟡 主动挂载已有远端轨道:', publication.track.kind)
|
|
|
|
+ attachRemoteTrack(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)
|
|
|
|
+ })
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ 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
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ leaveRoom() {
|
|
|
|
+ if (this.room) {
|
|
|
|
+ this.localTracks.forEach(track => {
|
|
|
|
+ track.stop()
|
|
|
|
+ track.detach()
|
|
|
|
+ })
|
|
|
|
+ this.room.disconnect()
|
|
|
|
+ this.room = null
|
|
|
|
+ this.localTracks = []
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+export {
|
|
|
|
+ createLocalAudioTrack,
|
|
|
|
+ createLocalVideoTrack,
|
|
|
|
+}
|
|
|
|
+export default new Trtc()
|