Closing WebRTC track will not close camera device or tab camera indicator

Viewed 603

Banging my head to the wall with this one, I can't seem to understand what is holding on the camera video stream and not closing when MediaStreamTrack.stop() called.

I have a typescript class where I handle getting the WebRTC stream track and passing it using an observable event to a functional reactjs component, the below code is the component registering to the event and using state for the stream track.

const [videoStreamTrack, setVideoStreamTrack] = useState < MediaStreamTrack > (
    null
)

useEffect(() => {
    return () => {
        videoStreamTrack?.stop()
        videoElement.current.srcObject.getVideoTracks().forEach((track) => {
            track.stop()
            videoElement.current.srcObject.removeTrack(track)
        })
        videoElement.current.srcObject = null
    }
}, [])

case RoomEvents.WebcamProducerAdded:
case RoomEvents.VideoStreamReplaced: {
  if (result.data?.track) {
      if (result.data.track.kind === 'video') {
        previewVideoStreamTrack?.stop()
        setPreviewVideoStreamTrack(null)
        setVideoStreamTrack(result.data.track)
      }
    }
  break
}

In the "Room" class I use the below code to grab the stream.

const videoDevice = this.webcam.device
if (!videoDevice) {
    throw new Error('no webcam devices')
}

const userMedia = await navigator.mediaDevices.getUserMedia({
    video: this.environmentPlatformService.isMobile ?
        true : {
            deviceId: {
                exact: this.webcam.device.deviceId
            },
            ...VIDEO_CONSTRAINS[this.webcam.resolution],
        },
})

const videoTrack = userMedia.getVideoTracks()[0]

this.eventSubject.next({
    eventName: RoomEvents.WebcamProducerAdded,
    data: {
        track: videoTrack,
    },
})

I am holding to this.webcam.device details using the code below.

async updateInputOutputMediaDevices(): Promise < MediaDeviceInfo[] > {
    await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
    })
    const devices = await navigator.mediaDevices.enumerateDevices()
    await this.updateWebcams(devices)
    await this.updateAudioInputs(devices)
    await this.updateAudioOutputs(devices)
    return devices
}

private async updateWebcams(devices: MediaDeviceInfo[]) {
    this.webcams = new Map < string, MediaDeviceInfo > ()

    for (const device of devices.filter((d) => d.kind === 'videoinput')) {
        this.webcams.set(device.deviceId, device)
    }

    const array = Array.from(this.webcams.values())

    this.eventSubject.next({
        eventName: RoomEvents.CanChangeWebcam,
        data: {
            canChangeWebcam: array.length > 1,
            mediaDevices: array,
        },
    })
}

Refreshing the page will close the camera and tab indicator.

2 Answers
useEffect(() => {
    return () => {
        videoStreamTrack?.stop()
        videoElement.current.srcObject.getVideoTracks().forEach((track) => {
            track.stop()
            videoElement.current.srcObject.removeTrack(track)
        })
        videoElement.current.srcObject = null
    }
}, [])

So here you are search and destroying video tracks. Seems right-ish; we'll see

async updateInputOutputMediaDevices(): Promise < MediaDeviceInfo[] > {
    await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
    })
    const devices = await navigator.mediaDevices.enumerateDevices()
    await this.updateWebcams(devices)
    await this.updateAudioInputs(devices)
    await this.updateAudioOutputs(devices)
    return devices
}

Above I see there's a call for audio might be where the hiccups are? Can't overly examine but maybe you're opening both and closing just video? Try doing a loop through all tracks not just video and see what's there?

@blanknamefornow answer helped me nail the issue.

  1. We are calling getUserMedia in multiple places not only in the “room” class handling mediasoup actions but also fore preview/device-selection/etc and didn’t really ever closed the tracks retrieved.
  2. Sometimes, those tracks are held into useState variables and when component unmounted if you try to access the variables they are already nulled by reactjs. The workaround is since the HTML elements are still referenced stop the track when needed. I believe this was the missing ingredient when trying to figure it out.
Related