You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Twilio Video结合Vue2开发多人会议:参与者断开致音视频轨道失效

解决方案

1. 拆分独立参与者子组件

将每个参与者的UI与逻辑拆分为单独的子组件(如ParticipantItem.vue),实现状态与DOM的隔离。父组件列表更新时,只有状态变化的子组件会触发更新,其他子组件因key匹配会被复用,不会销毁重建,从而保留已绑定的音视频轨道。

子组件示例:

<template>
  <div class="twilio-attendant-col" v-show="participant.connected">
    <div class="twilio-attendant">
      <div v-if="!participant.video || !videoEnabled" class="twilio-avatar">{{ participant.name }}</div>
      <div v-show="participant.video || !videoEnabled" class="twilio-video" ref="mediaContainer"></div>
      <div class="twilio-attendant-controls">
        <button @click="$emit('toggle-audio', participant.identity)" class="btn">
          <i v-if="participant.audio" class="fa fa-microphone"></i>
          <i v-else class="fa fa-microphone-slash"></i>
        </button>
        <button v-if="videoEnabled" @click="$emit('toggle-video', participant.identity)" class="btn">
          <i class="fa fa-camera position-relative">
            <div v-if="!participant.video" class="icon-slash"></div>
          </i>
        </button>
        <button @click="$emit('toggle-pinned', participant.identity)" class="btn">
          <i class="fa fa-thumb-tack"></i>
        </button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ParticipantItem',
  props: {
    participant: {
      type: Object,
      required: true
    },
    videoEnabled: {
      type: Boolean,
      default: true
    }
  },
  mounted() {
    this.attachExistingTracks()
  },
  watch: {
    'participant.video'(newVal) {
      const videoEl = this.$refs.mediaContainer?.querySelector('video')
      if (videoEl) videoEl.style.display = newVal ? 'block' : 'none'
    },
    'participant.audio'(newVal) {
      const audioEl = this.$refs.mediaContainer?.querySelector('audio')
      if (audioEl) audioEl.muted = !newVal
    }
  },
  methods: {
    attachExistingTracks() {
      const tracks = this.$parent.tracksMap[this.participant.identity] || []
      tracks.forEach(track => this.attachTrack(track))
    },
    attachTrack(track) {
      if (!this.$refs.mediaContainer) return
      const existingEl = this.$refs.mediaContainer.querySelector(`[data-track-id="${track.id}"]`)
      if (!existingEl) {
        const mediaEl = track.attach()
        mediaEl.dataset.trackId = track.id
        this.$refs.mediaContainer.appendChild(mediaEl)
      }
    }
  },
  beforeDestroy() {
    const mediaEls = this.$refs.mediaContainer?.querySelectorAll('video, audio') || []
    mediaEls.forEach(el => {
      const trackId = el.dataset.trackId
      if (trackId) {
        const track = Array.from(this.$parent.room.participants.values())
          .flatMap(p => Array.from(p.tracks.values()))
          .find(t => t.track.id === trackId)?.track
        track?.detach(el)
      }
    })
  }
}
</script>

2. 父组件改造

在父组件中引入子组件,同时维护稳定的参与者对象引用与轨道映射,避免组件不必要的销毁重建:

<template>
  <div class="attendants-window">
    <ParticipantItem
      v-for="item in participants"
      :key="item.identity"
      :participant="item"
      :video-enabled="videoEnabled"
      @toggle-audio="onToggleAudio"
      @toggle-video="onToggleVideo"
      @toggle-pinned="onTogglePinned"
      ref="participantItems"
    />
  </div>
</template>

<script>
import ParticipantItem from './ParticipantItem.vue'

export default {
  components: { ParticipantItem },
  data() {
    return {
      participants: [],
      tracksMap: {}, // 存储每个参与者的轨道集合:{ identity: [track1, track2] }
      videoEnabled: true,
      room: null // Twilio Room实例
    }
  },
  methods: {
    handleParticipantJoined(participant) {
      // 复用已有参与者对象,避免新建
      const existing = this.participants.find(p => p.identity === participant.identity)
      if (existing) {
        existing.connected = true
      } else {
        this.participants.push({
          identity: participant.identity,
          name: participant.identity,
          connected: true,
          audio: true,
          video: true,
          pinned: false
        })
      }

      // 监听轨道订阅
      participant.on('trackSubscribed', track => {
        this.tracksMap[participant.identity] = this.tracksMap[participant.identity] || []
        this.tracksMap[participant.identity].push(track)
        
        const targetItem = this.$refs.participantItems.find(item => item.participant.identity === participant.identity)
        targetItem?.attachTrack(track)
        
        this.$set(this.participants.find(p => p.identity === participant.identity), track.kind, true)
      })

      // 监听轨道取消订阅
      participant.on('trackUnsubscribed', track => {
        const targetItem = this.$refs.participantItems.find(item => item.participant.identity === participant.identity)
        const el = targetItem?.$refs.mediaContainer?.querySelector(`[data-track-id="${track.id}"]`)
        if (el) {
          track.detach(el)
          el.remove()
        }
        
        this.$set(this.participants.find(p => p.identity === participant.identity), track.kind, false)
        this.tracksMap[participant.identity] = this.tracksMap[participant.identity].filter(t => t.id !== track.id)
      })
    },
    handleParticipantDisconnected(participant) {
      // 修改connected状态而非删除对象,保留组件实例
      const target = this.participants.find(p => p.identity === participant.identity)
      if (target) target.connected = false
    },
    onToggleAudio(identity) {
      // 原有音频切换逻辑
    },
    onToggleVideo(identity) {
      // 原有视频切换逻辑
    },
    onTogglePinned(identity) {
      // 固定功能实现:修改pinned属性后排序数组
      const target = this.participants.find(p => p.identity === identity)
      if (target) {
        target.pinned = !target.pinned
        this.participants = [...this.participants.sort((a, b) => b.pinned - a.pinned)]
      }
    }
  }
}
</script>

3. 核心优化要点

  • 稳定对象引用:更新参与者状态时直接修改现有对象属性(如connected、pinned),而非删除后重新添加,避免组件销毁重建。
  • 子组件隔离:每个参与者的DOM与轨道绑定逻辑独立,父组件列表变化时仅更新状态变更的子组件,复用其他子组件以保留音视频轨道。
  • 轨道映射管理:通过tracksMap维护轨道引用,确保组件挂载或复用时能重新绑定轨道,避免丢失。
  • 响应式合规:使用$set确保对象属性的响应式更新,通过子组件生命周期与watch处理DOM操作,替代直接的jQuery DOM操作。

内容的提问来源于stack exchange,提问作者andrejmatulik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 21:55:15