Skip to content
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

fix(youtube-player): memory leak if player is destroyed before it is done initializing #18046

Merged
merged 1 commit into from
Jan 22, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions src/youtube-player/youtube-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,12 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
heightObs,
this.createEventsBoundInZone(),
this._ngZone
).pipe(waitUntilReady(), takeUntil(this._destroyed), publish());
).pipe(waitUntilReady(player => {
// Destroy the player if loading was aborted so that we don't end up leaking memory.
if (!playerIsReady(player)) {
player.destroy();
}
}), takeUntil(this._destroyed), publish());

// Set up side effects to bind inputs to the player.
playerObs.subscribe(player => this._player = player);
Expand Down Expand Up @@ -438,39 +443,43 @@ function bindSuggestedQualityToPlayer(
/**
* Returns an observable that emits the loaded player once it's ready. Certain properties/methods
* won't be available until the iframe finishes loading.
* @param onAbort Callback function that will be invoked if the player loading was aborted before
* it was able to complete. Can be used to clean up any loose references.
*/
function waitUntilReady(): OperatorFunction<UninitializedPlayer | undefined, Player | undefined> {
function waitUntilReady(onAbort: (player: UninitializedPlayer) => void):
OperatorFunction<UninitializedPlayer | undefined, Player | undefined> {
return flatMap(player => {
if (!player) {
return observableOf<Player|undefined>(undefined);
}
if ('getPlayerStatus' in player) {
if (playerIsReady(player)) {
return observableOf(player as Player);
}

// Since removeEventListener is not on Player when it's initialized, we can't use fromEvent.
// The player is not initialized fully until the ready is called.
return fromPlayerOnReady(player)
.pipe(take(1), startWith(undefined));
});
}
return new Observable<Player>(emitter => {
let aborted = false;
let resolved = false;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this whole block feels a little dirty, but it's a side-effect of the way the component was set up to use observables for everything. It could be done cleaner, but we'd have to move a bunch of internals around.

const onReady = (event: YT.PlayerEvent) => {
resolved = true;

if (!aborted) {
event.target.removeEventListener('onReady', onReady);
emitter.next(event.target);
}
};

/** Since removeEventListener is not on Player when it's initialized, we can't use fromEvent. */
function fromPlayerOnReady(player: UninitializedPlayer): Observable<Player> {
return new Observable<Player>(emitter => {
let aborted = false;
player.addEventListener('onReady', onReady);

const onReady = (event: YT.PlayerEvent) => {
if (aborted) {
return;
}
event.target.removeEventListener('onReady', onReady);
emitter.next(event.target);
};

player.addEventListener('onReady', onReady);
return () => {
aborted = true;

return () => {
aborted = true;
};
if (!resolved) {
onAbort(player);
}
};
}).pipe(take(1), startWith(undefined));
});
}

Expand Down Expand Up @@ -584,7 +593,12 @@ function bindCueVideoCall(
}

function hasPlayerStarted(player: YT.Player): boolean {
return [YT.PlayerState.UNSTARTED, YT.PlayerState.CUED].indexOf(player.getPlayerState()) === -1;
const state = player.getPlayerState();
return state !== YT.PlayerState.UNSTARTED && state !== YT.PlayerState.CUED;
}

function playerIsReady(player: UninitializedPlayer): player is Player {
return 'getPlayerStatus' in player;
}

/** Combines the two observables temporarily for the filter function. */
Expand Down