[Webkit-unassigned] [Bug 277537] New: Video element stalls even after stalled video track removed

bugzilla-daemon at webkit.org bugzilla-daemon at webkit.org
Fri Aug 2 01:14:02 PDT 2024


https://bugs.webkit.org/show_bug.cgi?id=277537

            Bug ID: 277537
           Summary: Video element stalls even after stalled video track
                    removed
           Product: WebKit
           Version: Safari 17
          Hardware: Mac (Apple Silicon)
                OS: macOS 14
            Status: NEW
          Severity: Normal
          Priority: P2
         Component: Media
          Assignee: webkit-unassigned at lists.webkit.org
          Reporter: szymon.witamborski at bibo.com.ph

When video track stops during RTC negotiation (either by user action or by device failure), the video element used to play remote tracks will stall and calls to `video.play()` methods will never resolve. This happens even after the video track is eventually removed from the element and the audio track is active and streaming data.

Context: in RTC negotiations, the `track` events for remote tracks on `RTCPeerConnection` are dispatched when the remote offer is received and local answer is created. Importantly, this happens before the P2P connection is established. That means that when the track is created there's no guarantee that it will ever produce a frame. But at the same time, it wouldn't be unreasonable for a WebRTC application the track is added to a `HTMLVideoElement.srcObject` the moment the `track` event is received.

This creates the condition for the element to stall and not play the valid (streaming) audio track once remaining negotiation steps complete. Track stopping on the sending end will trigger renegotiation but even when that triggers removal of the video track on the receiving end, the element remains stalled.

The steps to reproduce use `canvas` to simulate the situation for brevity and seems to replicate the behavior with RTC closely.

When the video track does produce a frame, removing the track works correctly. (And of course the video plays even before the track is removed.) So if this is considered expected behavior then this bug report operates on the assumption that it would be reasonable to expect the same result when a stalled track is removed. Please let me know whether this assumption is correct.

Notes:
1. The stalled remote track is indistinguishable from an active one, it's not `muted`, it's `enabled` and `readyState` is `live`. (Note that this is where `canvas`-based example differs, the track is muted until the first frame.)
2. The example code doesn't call `play()` when the element is created initially for brevity but the results there would be that it's going to stall until the call to `load()` where it will reject with an error.
3. Chrome 127 and Firefox 128 have similar issue. Small difference in Firefox is that audio plays but the `play()` method still doesn't resolve. Neither needs the `setTimeout` for a repro.
4. Safari needs the `new Promise(resolve => setTimeout(resolve, 0))` line in order for a repro. Presumably, some processing needs to happen before the track stalls. It's not necessary when the track comes from RTC though.

------------------

Steps to reproduce:

Interactive version: https://codepen.io/brainshave/pen/abgJydJ?editors=1011 (make sure "Console" is open)

1. Open new tab and go to `about:blank`
2. Run below code via dev tools:

```
async function test() {
  // Create sine wave track
  const audioContext = new AudioContext();
  const audioNode = audioContext.createMediaStreamDestination();
  const audioTrack = audioNode.stream.getAudioTracks()[0];
  const oscillator = audioContext.createOscillator();
  oscillator.type = "sine";
  oscillator.frequency.value = 412;
  oscillator.connect(audioNode);
  oscillator.start();

  // Create stalled video track
  const canvas = document.createElement('canvas');
  const videoTrack = canvas.captureStream().getTracks()[0]; 

  // Necessary for repro in Safari, can be skipped in Chrome/Firefox
  await new Promise(resolve => setTimeout(resolve, 0));

  const video = document.createElement('video');
  video.srcObject = new MediaStream([videoTrack, audioTrack]);
  await new Promise(resolve => video.addEventListener('loadstart', resolve, { once: true }));

  // Control: non-stalled video track doesn't have this problem
  // canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
  // await video.play();

  console.log('removing track');
  video.srcObject.removeTrack(videoTrack);

  // video.load();

  await video.play();
  console.log('playing');
}

test()
```

This should print only `removing track` on the console and no sound will play

3. Uncomment the `video.load()` and run again, this time `removing track` will be followed by  `playing` and sound should be heard
4. Reload the page to stop the sound
5. Comment the `video.load()` and uncomment the two lines starting with `canvas.getContext` and the following `await video.play()` line
6. Run this and again, `removing track` will be followed by `playing` and audio should be audible
7. Reload the page to stop the sound

The expected result would be `await video.play()` to resolve and `playing` text to show without the `video.load()` workaround.

-- 
You are receiving this mail because:
You are the assignee for the bug.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.webkit.org/pipermail/webkit-unassigned/attachments/20240802/836e2fac/attachment-0001.htm>


More information about the webkit-unassigned mailing list