twilio-video.js icon indicating copy to clipboard operation
twilio-video.js copied to clipboard

"hearbeat" messages are still sent even after disconnecting LocalParticipant from the room

Open DmitryMorozov228 opened this issue 3 years ago • 4 comments

  • [x] I have verified that the issue occurs with the latest twilio-video.js release and is not marked as a known issue in the CHANGELOG.md.
  • [x] I reviewed the Common Issues and open GitHub issues and verified that this report represents a potentially new issue.
  • [x] I verified that the Quickstart application works in my environment.
  • [x] I am not sharing any Personally Identifiable Information (PII) or sensitive account information (API keys, credentials, etc.) when reporting this issue.

Code to reproduce the issue:

class Room extends Emitter {
  public static attachTracks (tracks: Track[], container: any) {
    tracks.forEach(track => {
      if (track.kind !== 'data' && track._isEnabled) {
        container && container.appendChild(track.attach())
      }
    })
  }

  public nativeRoom = null
  private dataTrack = null
  private container = null

  constructor ({ room, dataTrack }) {
    super()

    this.nativeRoom = room
    this.dataTrack = dataTrack
    // TODO: need to develop
    console.log('roomJoined', room)
    // When a Participant adds a Track, attach it to the DOM.
    room.on('trackSubscribed', (...p) => this.emit('trackSubscribed', ...p))
    // When a Participant removes a Track, detach it from the DOM.
    room.on('trackUnsubscribed', (track: Track, publication: TrackPublication, participant: Participant) => {
      this.detachTracks([track])
      this.emit('trackUnsubscribed', track, publication, participant)
    })
    // When a RemoteTrack was enabled by a RemoteParticipant in the Room
    this.nativeRoom.on('trackEnabled', (publication: TrackPublication, participant: Participant) => {
      Room.attachTracks([publication.track], this.container)
      this.emit('trackEnabled', publication, participant)
    })
    // When a RemoteTrack was disabled by a RemoteParticipant in the Room
    room.on('trackDisabled', (publication: TrackPublication, participant: Participant) => {
      this.detachTracks([publication.track])
      this.emit('trackDisabled', publication, participant)
    })
    // When a RemoteParticipant left the Room, detach RemoteParticipant's Tracks.
    room.on('participantDisconnected', (participant: Participant) => {
      this.detachParticipantTracks(participant)
      this.emit('participantDisconnected', participant)
    })
    // Once the LocalParticipant left the room, detach the Tracks
    // of all Participants, including that of the LocalParticipant.
    room.on('disconnected', (room: TwilioVideoRoom, error: any) => {
      room.localParticipant.tracks.forEach(publication => {
        publication.track.stop()
      })
      this.detachParticipantTracks(room.localParticipant)
      this.emit('disconnected', room, error)
    })
    this.nativeRoom.participants.forEach(participant => {
      participant.on('trackSubscribed', (track: Track, publication: TrackPublication) => {
        this.onTrackSubscribed(track)
      })
    })
  }

  public detachTracks (tracks: Track[]) {
    tracks.forEach(track => {
      if (track.kind === 'audio' || track.kind === 'video') {
        track.detach().forEach(detachedElement => {
          detachedElement.remove()
        })
      }
    })
  }

  // Attach the Participant's Tracks to the DOM.
  public attachParticipantTracks (participant: Participant, container: any) {
    const tracks = this.getSubscribedParticipantTracks(participant)
    Room.attachTracks(tracks, container)
  }

  // Detach the Participant's Tracks from the DOM.
  public detachParticipantTracks (participant: Participant) {
    const tracks = this.getSubscribedParticipantTracks(participant)
    this.detachTracks(tracks)
  }

  private getSubscribedParticipantTracks = (participant: Participant) => {
    return Array.from<TrackPublication>(participant.tracks.values())
      .reduce((acc, publication) => {
        if (publication.track) {
          acc.push(publication.track)
        }
        return acc
      }, [])
  }

  public onEnableMicrophone () {
    this.nativeRoom.localParticipant.audioTracks.forEach(audioTrackPublication => {
      audioTrackPublication.track.enable()
    })
  }

  public onDisableMicrophone () {
    this.nativeRoom.localParticipant.audioTracks.forEach(audioTrackPublication => {
      audioTrackPublication.track.disable()
    })
  }

  public onStart (container, { activeCall }) {
    return new Promise(resolve => {
      this.container = container
      let remoteAddedTracksCount = 0
      const onTrackSubscribed = (track, participantRole) => {
        this.onTrackSubscribed(track)
        this.attachTrack(container, track)
        if (participantRole === USER_ROLES.fieldTehnical) {
          remoteAddedTracksCount += 1
          if (remoteAddedTracksCount >= REMOTE_TRACKS_COUNT) {
            resolve()
          }
        }
      }
      this.nativeRoom.participants.forEach(participant => {
        const participantRole = participant.identity.split(':')[1]
        this.attachParticipantTracks(participant, container)
        // if refresh page
        participant.on('trackSubscribed', (track: Track, publication: TrackPublication) => {
          onTrackSubscribed(track, participantRole)
        })
        // if user moved to another page
        if (participantRole === USER_ROLES.fieldTehnical) {
          const remoteTrackPublications = Array.from<TrackPublication>(participant.tracks.values())
            .filter(publication => !!publication.track)
          if (remoteTrackPublications.length >= REMOTE_TRACKS_COUNT) {
            resolve()
          }
        }
      })
      this.nativeRoom.on('participantConnected', participant => {
        const participantRole = participant.identity.split(':')[1]
        participant.tracks.forEach(publication => {
          // If the TrackPublication is already subscribed to, then attach the Track to the DOM.
          if (publication.isSubscribed) {
            onTrackSubscribed(publication.track, participantRole)
          }
        })
        // Once the Track is subscribed to, attach the Track to the DOM.
        participant.on('trackSubscribed', track => {
          onTrackSubscribed(track, participantRole)
        })
      })

      this.nativeRoom.localParticipant.audioTracks.forEach(audioTrackPublication => {
        if (_get(activeCall, ['properties', 'isMutedMicrophone'])) {
          audioTrackPublication.track.disable()
        } else {
          audioTrackPublication.track.enable()
        }
      })
    })
  }

  private onTrackSubscribed = track => {
    if (track.kind === TRACK_TYPES.data) {
      track.on('message', event => {
        const data = JSON.parse(event)
        const { id, meta: { role } } = data

        if (deliveryManager.hasSending(id)) {
          deliveryManager.get(id).resolve()
          deliveryManager.remove(id)
        } else {
          this.emit('message', data)
          if (role !== USER_ROLES.wearableDeviceUser) {
            return
          }
          this.onSendToDataTrack({ id: data.id, meta: { role: USER_ROLES.computerUser }, type: MESSAGE_TYPES.received })
        }
      })
      return
    }
  }

  private attachTrack = (container, track) => {
    if (track._isEnabled) {
      container.appendChild(track.attach())
    }
  }

  public onHold () {
    this.onDisableMicrophone()
    this.nativeRoom.participants.values().forEach(participant => {
      this.detachTracks(participant.tracks)
    })
    this.container = null
  }

  public onEnd () {
    this.nativeRoom.disconnect()
  }

  private onSendToDataTrack (data) {
    this.dataTrack.send(JSON.stringify(data))
  }

  public onSendMessage (data) {
    const messageId = data.id || uuid()
    this.onSendToDataTrack({ ...data, id: messageId })
    const checkDelivery = (resolve, reject) => {
      deliveryManager.send({ id: messageId, isDelivered: false, resolve, reject })
      setTimeout(() => {
        if (deliveryManager.get(messageId)) {
          deliveryManager.remove(messageId)
        }

        return reject(`Message with id:${messageId} was not sent over the DataTrack`)
      }, TIME_TO_MESSAGE_DELIVERY)
    }
    return new Promise(checkDelivery)
  }

  public onSendDeliveredMessage (data) {
    const messageId = data.id || uuid()
    return new Promise((resolve, reject) => {
      deliveryManager.send({ id: messageId, isDelivered: true, resolve, reject })
      resolve()
    })
  }
}
export default Room

Expected behavior:

LocalParticipant should be disconnected from the Group/P2P room and shouldn't get any "hearbeat" messages.

Actual behavior:

Incoming and outgoing "hearbeat" messages are still sent even after disconnecting LocalParticipant from the Group/P2P room.

Logs: frontend-app-twilio-logs

Software versions:

  • [x] Browser(s): 87.0.4280.141
  • [x] Operating System: Windows
  • [x] twilio-video.js: 2.7.2
  • [x] Third-party libraries (e.g., Angular, React, etc.): ReactJS

DmitryMorozov228 avatar Jan 13 '21 11:01 DmitryMorozov228

Hey @DmitryMorozov228 , thank you for reporting! Do you have an example code on how you would use the class you provided?

charliesantos avatar Jan 15 '21 00:01 charliesantos

Hi @charliesantos , The samples of code here:

1.
export const removeCallHandlersById = ({ id }) => (dispatch, getState) => {
  const room = roomManager.getRoomById(id)
  // disconnect from the Twilio room
  room && room.onEnd()
  dispatch(onRemoveCalls([id]))
  roomManager.removeRoom({ id })
  keepAliveManager.remove({ id })
}

2.
export const onToggleMicrophone = () => (dispatch, getState) => {
  const state = getState()
  const { id, properties }: any = getActiveCall(state)
  const isMutedMicrophone = !properties.isMutedMicrophone
  const room = roomManager.getRoomById(id)
  if (isMutedMicrophone) {
    // mute Local Participant microphone
    room.onDisableMicrophone()
  } else {
    // unmute Local Participant microphone
    room.onEnableMicrophone()
  }
  dispatch(updateCallById({
    id,
    properties: {
      isMutedMicrophone,
    },
  }))
}
3.
// Send data to the DataTrack
room.onSendMessage(data)
    .then(() => {
      dispatch(markMessageAsSent({ id, messageId: data.id }))
    })
    .catch(() => {
      dispatch(markMessageAsNotSent({ id, messageId: data.id }))
    })

DmitryMorozov228 avatar Jan 15 '21 12:01 DmitryMorozov228

Hey @DmitryMorozov228 thanks for the example code. We are tracking this internally now (JSDK-3125) and will investigate for a fix.

charliesantos avatar Jan 20 '21 05:01 charliesantos

Hi @charliesantos , Any updates on this?

DmitryMorozov228 avatar May 13 '21 12:05 DmitryMorozov228