-
Notifications
You must be signed in to change notification settings - Fork 216
video-6336: media element leak causes error on chrome 92 #1530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4e19a9d
bb3bbf3
b3cac3a
85f8786
6d445c2
ac99749
1c36889
b807e6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -145,8 +145,10 @@ class MediaTrack extends Track { | |
_end() { | ||
this._log.debug('Ended'); | ||
if (this._dummyEl) { | ||
this._detachElement(this._dummyEl); | ||
this._dummyEl.remove(); | ||
this._dummyEl.srcObject = null; | ||
this._dummyEl.oncanplay = null; | ||
this._dummyEl = null; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice one @makarandp0 . So we are only detaching elements, not properly disposing them. I've learned that we also need to pause the element first, then, call load after setting the srcObject to null. Something like this._detachElementInternal(this._dummyEl);
this._dummyEl.pause();
this._dummyEl.remove();
this._dummyEl.srcObject = null;
this._dummyEl.oncanplay = null;
this._dummyEl.load();
this._dummyEl = null; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay, I will update and test per your suggestion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After experimenting a bit, I found that just setting srcObject to null is sufficient to avoid this issue. I will keep this simple and just reset |
||
} | ||
|
||
|
@@ -165,7 +167,6 @@ class MediaTrack extends Track { | |
: null; | ||
this._elShims.set(el, shimMediaElement(el, onUnintentionallyPaused)); | ||
} | ||
|
||
return el; | ||
} | ||
|
||
|
@@ -266,12 +267,10 @@ class MediaTrack extends Track { | |
if (!this._attachments.has(el)) { | ||
return el; | ||
} | ||
|
||
const mediaStream = el.srcObject; | ||
if (mediaStream instanceof this._MediaStream) { | ||
mediaStream.removeTrack(this.processedTrack || this.mediaStreamTrack); | ||
} | ||
|
||
this._attachments.delete(el); | ||
|
||
if (this._shouldShimAttachedElements && this._elShims.has(el)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* eslint-disable no-undefined */ | ||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const defaults = require('../../lib/defaults'); | ||
const { completeRoom, createRoom } = require('../../lib/rest'); | ||
const { audio: createLocalAudioTrack, video: createLocalVideoTrack } = require('../../../lib/createlocaltrack'); | ||
const connect = require('../../../lib/connect'); | ||
const getToken = require('../../lib/token'); | ||
const { isChrome } = require('../../lib/guessbrowser'); | ||
|
||
const { | ||
smallVideoConstraints, | ||
randomName, | ||
participantsConnected, | ||
tracksSubscribed, | ||
tracksUnpublished, | ||
waitForSometime, | ||
waitFor, | ||
trackStarted | ||
} = require('../../lib/util'); | ||
|
||
|
||
function getTracksOfKind(participant, kind) { | ||
return [...participant.tracks.values()].filter(remoteTrack => remoteTrack.kind === kind).map(({ track }) => track); | ||
} | ||
|
||
// eslint-disable-next-line no-warning-comments | ||
// TODO(mpatwardhan): Fix VIDEO-6356 and enable this test for p2p rooms. | ||
(isChrome && defaults.topology !== 'peer-to-peer' ? describe : describe.skip)('VIDEO-6336: media element leak detection', function() { | ||
// eslint-disable-next-line no-invalid-this | ||
this.timeout(5 * 60 * 1000); | ||
|
||
it('tracks continue to start after 50 connect disconnects ', async () => { | ||
const localAudioTrack = await createLocalAudioTrack({ fake: true }); | ||
const localVideoTrack = await createLocalVideoTrack(smallVideoConstraints); | ||
const roomName = await createRoom(randomName(), defaults.topology); | ||
const connectOptions = Object.assign({ | ||
name: roomName, | ||
tracks: [localAudioTrack, localVideoTrack] | ||
}, defaults); | ||
|
||
const aliceRoom = await waitFor(connect(getToken('Alice'), connectOptions), 'Alice to connect to room'); | ||
async function joinRoomAndEnsureTracksStarted(i) { | ||
// eslint-disable-next-line no-console | ||
console.log(`${i}] connecting to room ${aliceRoom.sid}`); | ||
const bobRoom = await waitFor(connect(getToken('Bob'), connectOptions), `${i}] Bob to join room: ${aliceRoom.sid}`); | ||
|
||
// wait for Bob to see alice connected. | ||
await waitFor(participantsConnected(bobRoom, 1), `${i}] Bob to see Alice connected: ${aliceRoom.sid}`); | ||
|
||
const aliceRemote = bobRoom.participants.get(aliceRoom.localParticipant.sid); | ||
await waitFor(tracksSubscribed(aliceRemote, 2), `${i}] Bob to see Alice's track: ${aliceRoom.sid}`); | ||
|
||
const remoteVideoTracks = getTracksOfKind(aliceRemote, 'video'); | ||
const remoteAudioTracks = getTracksOfKind(aliceRemote, 'audio'); | ||
assert.strictEqual(remoteVideoTracks.length, 1); | ||
assert.strictEqual(remoteAudioTracks.length, 1); | ||
|
||
await waitFor(trackStarted(remoteVideoTracks[0]), `${i}] Bob to see Alice's video track started: ${aliceRoom.sid}`); | ||
await waitFor(trackStarted(remoteAudioTracks[0]), `${i}] Bob to see Alice's audio track started: ${aliceRoom.sid}`); | ||
bobRoom.disconnect(); | ||
await waitForSometime(1000); | ||
} | ||
|
||
// Alice joins room first. | ||
for (let i = 0; i < 50; i++) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await joinRoomAndEnsureTracksStarted(i); | ||
} | ||
|
||
aliceRoom.disconnect(); | ||
await completeRoom(aliceRoom.sid); | ||
}); | ||
|
||
it('tracks continue to start after 50 publish/unpublish ', async () => { | ||
const localAudioTrack = await createLocalAudioTrack({ fake: true }); | ||
const localVideoTrack = await createLocalVideoTrack(smallVideoConstraints); | ||
const roomName = await createRoom(randomName(), defaults.topology); | ||
const connectOptions = Object.assign({ | ||
name: roomName, | ||
tracks: [] | ||
}, defaults); | ||
|
||
const aliceRoom = await waitFor(connect(getToken('Alice'), connectOptions), 'Alice to connect to room'); | ||
const bobRoom = await waitFor(connect(getToken('Bob'), connectOptions), `Bob to join room: ${aliceRoom.sid}`); | ||
// wait for Bob to see alice connected. | ||
await waitFor(participantsConnected(bobRoom, 1), `Bob to see Alice connected: ${aliceRoom.sid}`); | ||
|
||
const aliceRemote = bobRoom.participants.get(aliceRoom.localParticipant.sid); | ||
|
||
async function publishAndVerifyStarted(i) { | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(`${i}] Alice publishing tracks`); | ||
// alice publishes two tracks. | ||
const audioPublication = await waitFor(aliceRoom.localParticipant.publishTrack(localAudioTrack), `Alice to publish a audio track: ${aliceRoom.sid}`); | ||
const videoPublication = await waitFor(aliceRoom.localParticipant.publishTrack(localVideoTrack), `Alice to publish a video track: ${aliceRoom.sid}`); | ||
|
||
await waitFor(tracksSubscribed(aliceRemote, 2), `${i}] Bob to see Alice's track: ${aliceRoom.sid}`); | ||
|
||
const remoteVideoTracks = getTracksOfKind(aliceRemote, 'video'); | ||
const remoteAudioTracks = getTracksOfKind(aliceRemote, 'audio'); | ||
assert.strictEqual(remoteVideoTracks.length, 1); | ||
assert.strictEqual(remoteAudioTracks.length, 1); | ||
|
||
// bob sees tracks published by alice started. | ||
await waitFor(trackStarted(remoteVideoTracks[0]), `${i}] Bob to see Alice's video track started: ${aliceRoom.sid}`); | ||
await waitFor(trackStarted(remoteAudioTracks[0]), `${i}] Bob to see Alice's audio track started: ${aliceRoom.sid}`); | ||
|
||
const bobSeesUnpublished = tracksUnpublished(aliceRemote, 2); | ||
|
||
// alice un-publishes tracks. | ||
audioPublication.unpublish(); | ||
videoPublication.unpublish(); | ||
|
||
// bob sees tracks unpublished. | ||
await waitFor(bobSeesUnpublished, `${i}] Bob to see tracks unpublished: ${aliceRoom.sid} `); | ||
|
||
await waitForSometime(1000); | ||
} | ||
|
||
for (let i = 0; i < 50; i++) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await publishAndVerifyStarted(i); | ||
} | ||
|
||
aliceRoom.disconnect(); | ||
await completeRoom(aliceRoom.sid); | ||
}); | ||
|
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,10 @@ class HTMLElement { | |
return this; | ||
} | ||
|
||
remove() { | ||
|
||
} | ||
|
||
play() { | ||
return Promise.resolve(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just want to double check. Since we're not calling detachElement anymore, is setting srcObject to null enough, and we don't need to call
removeTrack
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thats correct, removeTrack applies only for elements attached by application - since they may already have a mediaStream, in that case we add/remove track from it.