16

I'd like to be informed about a MediaStreamTrack's end. According to MDN an ended event is

Sent when playback of the track ends (when the value readyState changes to ended). Also available using the onended event handler property.

So I should be able to setup my callback(s) like:

const [track] = stream.getVideoTracks();
track.addEventListener('ended', () => console.log('track ended'));
track.onended = () => console.log('track onended');

and I expect those to be invoked, once I stop the track via:

tracks.forEach(track => track.stop());
// for good measure? See
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop#Stopping_a_video_stream
videoElem.srcObject = null;

The problem I'm having is that the callbacks are not invoked. I built the following JSFiddle, where 3 MediaStreams are created in 3 different ways:

  1. getUserMedia
  2. getDisplayMedia
  3. getCaptureStream (canvas element)

I also have 3 buttons which stop all tracks for the respective MediaStream. The behaviour is as follows:

  • All 3 streams are inactive, the MediaStream's oninactive callback is triggered (in Chrome, seems like Firefox doesn't support this).
  • All tracks have a readyState of ended after being stopped.
  • If I stop the screen stream (2. getDisplayMedia) via Chrome's UI, the track ended callback(s) are invoked.

Chrome's stop sharing button

I know that you have to watch out for the track being used by multiple sources, but that shouldn't be the case here, right? Am I missing something obvious?

Since multiple tracks may use the same source (for example, if two tabs are using the device's microphone), the source itself isn't necessarily immediately stopped. It is instead disassociated from the track and the track object is stopped. Once no media tracks are using the source, the source may actually be completely stopped.

wpp
  • 7,093
  • 4
  • 33
  • 65

1 Answers1

22

Why is the 'ended' event not firing for this MediaStreamTrack?

Because ended is explicitly not fired when you call track.stop() yourself. It only fires when a track ends for other reasons. From the spec:

Fired when...

The MediaStreamTrack object's source will no longer provide any data, either because the user revoked the permissions, or because the source device has been ejected, or because the remote peer permanently stopped sending data.

This is by design. The thinking was you don't need an event when you stop it yourself. To work around it do:

track.stop();
track.dispatchEvent(new Event("ended"));

MediaStream's oninactive callback is triggered (in Chrome, seems like Firefox doesn't support this).

stream.oninactive and the inactive event are deprecated, and no longer in the spec.

As a workaround for that, you can use the similar ended event on a media element:

video.srcObject = stream;
await new Promise(resolve => video.onloadedmetadata = resolve);
video.addEventListener("ended", () => console.log("inactive!")); 

Alas, that does not appear to work in Chrome yet, but it works in Firefox.

jib
  • 40,579
  • 17
  • 100
  • 158
  • Thanks a lot for the clarification and the work-around. I might dispatch "stopped" instead, to avoid further confusion. Guess I got hung up on "when the value readyState changes to ended" and IMO "ended" might not be the clearest name for the event. – wpp May 03 '19 at 07:06
  • 1
    @wpp Well, `"ended"` will still be fired when a user clicks the *Stop sharing* button in the browser UI itself, or when remote tracks from peer connections end, so only use a different name if you don't care about those events, or need to distinguish them from your own. – jib May 03 '19 at 12:37
  • Thanks for the detailed answer. This is really unfortunate. :-( I think that reasoning on the 'ended' event is flawed. Now we have to manage state separately and do things twice. Very unfortunate too that the `inactive` event is deprecated... that's the only way to observe a stream created by other code, short of monkey-patching everything. Do you know the reasoning behind removing the `inactive` event for streams? – Brad Apr 02 '20 at 02:25
  • 1
    @Brad Focus shifted to tracks, which streams are vessels for. You can polyfill [inactive](https://w3c.github.io/mediacapture-main/getusermedia.html#stream-inactive) using `stream.onremovetrack` and checking `readyState` of its remaining tracks. The definition was also limited since an audioElement.srcObject was technically [active](https://w3c.github.io/mediacapture-main/getusermedia.html#stream-active) even if it only contained video tracks. – jib Apr 02 '20 at 04:16
  • @jib Ah cool, I didn't realize a track was removed when it was stopped. I'll experiment with that. Thank you for the extra information! Greatly appreciated. – Brad Apr 02 '20 at 04:38
  • @jib Unfortunately it seems that `removetrack` isn't fired when tracks from getUserMedia are stopped. I'll hack it from other ways. Thanks for the info though. – Brad Apr 02 '20 at 05:23
  • @Brad You're right, you'd need to listen `track.onended` as well. Even then you'd need to catch JS calling `track.stop()` yourself since that doesn't fire `ended`. – jib Apr 02 '20 at 16:51
  • @jib In my project, I want to send audio with screen sharing so I initiated 2 streams and in screen sharing stream, I added audio track. It is working fine but now I am not able to track event when stream get inactive. Previously without starting audio track **screensharestream.oninactive** function was working but now its not working. Can you please help me to resolve this issue? – Arjun May 05 '20 at 12:23
  • @Arjun `oninactive` is not web compatible, so please stop using it. Did you try the `ended` event as shown in this answer? If that didn't help, consider posting a new question. – jib May 07 '20 at 00:18
  • On Firefox `track.dispatchEvent(new Event("ended"));` does not work if one uses `track.addEventListener('ended', …)`. See [Firefox bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1473457). – Semmel Jun 28 '23 at 15:17