Will Producer.close() close the underlying MediaStreamTrack?

The reactjs app I am working on sometimes rerenders components reusing the same MediaStreamTrack. When I close the session (you can call it room or whatever) I invoke close in the websocket connection, producers, and transports (see below) but my browser notification and the camera indicator light is on (in some cases).

Playing around with it I discovered that actually invoking track.stop on the videoElement HTML component and videoElement.current.srcObject = null will close the indicator light of the camera after a couple of seconds (or more) which gives me the impression that Producer.close() does not! I just assumed it will.

if (videoElement.current.paused && !isVideoPlaying && videoCanPlay) {
        if (videoElement.current.srcObject) {
          if (videoElement.current.srcObject?.getVideoTracks()) {
            videoElement.current.srcObject.getVideoTracks().forEach((track) => {
              track.stop()
            })
          }
          videoElement.current.srcObject = null
        }

Could you please clarify if the MediaStreamTrack will actually close calling Producer.close()?

async close(): Promise<void> {
    if (this.protooPeer && !this.protooPeer.closed) {
      // Close protoo Peer
      this.protooPeer?.close()
    }

    // Close mediasoup Transports.
    if (this.sendTransport && !this.sendTransport.closed) {
      this.sendTransport.close()
      this.sendTransport = null
    }

    if (this.recvTransport && !this.recvTransport.closed) {
      this.recvTransport.close()
      this.recvTransport = null
    }

    // Close mediasoup Producers.
    if (this.micProducer && !this.micProducer.closed) {
      this.micProducer.close() // Will close audio MediaStreamTrack?
      this.micProducer = null
    }

    if (this.videoProducer && !this.videoProducer.closed) {
      this.videoProducer.close() // Will close video MediaStreamTrack?
      this.videoProducer = null
    }
  }

We write documentation for something. Those are the ProducerOptions:

Also, checking the source code also helps:

1 Like

I will check my options, seems like the default should have closed it. Thanks.

Yes, it should.

Revisiting this thread as I got back to try fixing it with no luck so far…

If you see the screenshot my tab camera use icon is still on after closing the producers and my actual computer camera is still on. Refreshing the tab will fix it so at this point I am open to suggestions on how to tackle this. See below what I tried.

I am using functional components and with useEffects and useState I set the MediaStreamTrack. e.g.

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.stream) {
          const videoTrack = result.data.stream.getVideoTracks()[0]
          if (videoTrack) {
            setVideoStreamTrack(videoTrack) // in turn other effects will be invoked to play the track in video element
          }
        }
        break
      }

Closing the producer.
Closing the track in the component when unmounted.
Closing the track in the video element.

The camera light in my macbook and the icon in the tab still on…

Screen Shot 2021-04-17 at 14.08.36

Since this is not really mediasoup related question I have created a SO post and paste it here for reference in case other people can help or get help.

If you do not close the transport you’re still connected and that’s what that live icon is.

  1. Check if broadcast was made or exists enough to clear it.

  2. Checking the mediastream for tracks opened to stop and unset them.

  3. Last and but not least in this excitement of coarse would just be to close the transport and unset it.

      if (Userlist.broadcast.has(handle)) {
             Userlist.broadcast.get(handle).stream.getTracks().forEach((track) => {
                 track.stop();
                 track = null;
             });
             Userlist.broadcast.get(handle).transport.close();
             Userlist.broadcast.get(handle).transport = null;
             Userlist.broadcast.delete(handle);
         }
    

If you’re switching out streams you won’t or shouldn’t need to close the transport. Consider this example one I use to 100% close streams. Through the Webrtc-internals you should see connection closed and any broadcasts on your end will stick around for a bit for diagnostic reasons as all consumes on clear will disappear from that list if done right.

Saw a few errors in your code so it’s not easy to really advise but to give a simple example, but do suggest reading the well documented API most of this is posted. Cheers.

Thank you for the reply and for providing input so I can start getting to the solution.

Below is the code I am closing pretty much transports, producers, and the WebSocket connection.

if (this.protooPeer && !this.protooPeer.closed) {
      logger.info('will close protooPeer')

      // Close protoo Peer
      this.protooPeer?.close()
    }

    // Close mediasoup Transports.
    if (this.sendTransport && !this.sendTransport.closed) {
      logger.info('sendTransport close')
      this.sendTransport.close()
      this.sendTransport = null
    }

    if (this.recvTransport && !this.recvTransport.closed) {
      logger.info('recvTransport close')
      this.recvTransport.close()
      this.recvTransport = null
    }

    // Close mediasoup Producers.
    if (this.micProducer && !this.micProducer.closed) {
      logger.info('audio producer close')
      this.micProducer.close()
      this.micProducer = null
    }

    if (this.videoProducer && !this.videoProducer.closed) {
      logger.info('video producer close')
      this.videoProducer.close()
      this.videoProducer = null
    }

And the code I grab the track.

const userMedia = await navigator.mediaDevices.getUserMedia({
    video: this.environmentPlatformService.isMobile ?
        true :
        {
            deviceId: {
                exact: this.webcam.device.deviceId
            },
            ...VIDEO_CONSTRAINS[this.webcam.resolution],
        },
})
// This is the track used for broadcast in mediasoup and in HTML video element for local purposes
// I don't keep the userMedia variable anywhere (Code changed from previous snippet where I was passing the stream to components
const videoTrack = userMedia.getVideoTracks()[0]

this.videoProducer = await this.sendTransport?.produce({
        track: videoTrack,
        encodings,
        codecOptions: {
          videoGoogleStartBitrate: 1000,
        },
        codec,
      })

To your points

  1. Check if broadcast was made or exists enough to clear it. - When you say broadcast can you be more specific, MediaStream, MediaStreamTrack, Producer;
  2. Checking the mediastream for tracks opened to stop and unset them. - Where would you do that; since I don’t keep the stream around but just pass the track to the functional component I am doing this for the video element tracks and the react component state variable
  3. Last and but not least in this excitement of coarse would just be to close the transport and unset it. - It is tough for me to understand the broadcast, transport, and handle types in your code. I posted my code for reference in the discussion

If you’re switching out streams you won’t or shouldn’t need to close the transport.

I am switching streams from a preview track using the navigator API in reactjs component but stop and clear as soon as I have the stream from another call and configured with mediasoup Transport to create the video Producer.

Saw a few errors in your code so it’s not easy to really advise but to give a simple example, but do suggest reading the well documented API most of this is posted. Cheers.

Please point out any error in the StackOverflow, it will greatly help me improve and find the issue. Open to suggestions.

@xplatsolutions thought I’d offer some suggestions:

Producer.close will call track.stop for the currently associated track (unless you have explicitly disabled this functionality). You don’t need to look at any other mediasoup API to get this done.

Calling track.stop for all tracks returned from getUserMedia will disable the recording icon. That’s all you need to do. You don’t need to null any references.

The reason your recording icon persists is that you have a leftover track somewhere that hasn’t been closed. Have you perhaps used track.clone and forgotten to close the clone?

One way to identify your issue is to log all the track ids returned from getUserMedia, and then log all the track ids that you close. Compare the list, find out what you didn’t close, and that’ll give you an idea of what part of your code to look at.

Hope this helps!

2 Likes

Also, I see you storing userMedia and userMedia.getVideoTracks()[0]

Make sure you close ALL tracks returned from getUserMedia. userMedia wont be garbage collected if you maintain a reference to it, and you might still be maintaining unused tracks? It’s possible you have to close the tracks even if you don’t maintain a reference to the userMedia object. Are there any other video or audio tracks returned by getUserMedia? Do you make any other calls to getUserMedia? That might be where your issue lies.

@dimoochka so many good suggestions and I believe I have plenty to work on the next couple of days to get to the bottom of it.

Actually, I just thought that I never close the audio track passed to the Producer and in this case, I use the MediaStream passed to other functional components.
I guess it wouldn’t keep the camera device light indicator on but maybe the tab icon; Might be not related.

I will do the trick with the list as you said and the other suggestions and get back to this thread. Thanks!

I overviewed the stack slightly and it honestly looked like you may be calling audio and not closing all the tracks cause it shows all throughout just video, but you return audio in one of the functions, didn’t trace it though to see if you clear that. Curious if this was the case.

1 Like

I found the issues holding the MediaStreamTrack reference open. It wasn’t just one thing.

  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.
  3. Close video and audio tracks always (but of course not the one used in mediasoup). In any case I added additional code to make sure the mediasoup track is stopped when the session ended or stream track replaced and make sure the previous track stopped if exists.

Yes, audio matters for the tab indicator.

Thank you both for your help and suggestions. @dimoochka @CosmosisT

Awesome, glad you got it figured out! Debugging is purgatory.

1 Like