diff --git a/packages/youtube-video-element/test/test.js b/packages/youtube-video-element/test/test.js index c0fc1e0..3ae8f9f 100644 --- a/packages/youtube-video-element/test/test.js +++ b/packages/youtube-video-element/test/test.js @@ -250,6 +250,48 @@ test('t parameter - case insensitive', async function (t) { t.equal(startParam, '171', 'start parameter is set from uppercase T parameter'); }); +test('destroys the player when disconnected', async function (t) { + const video = await createVideoElement(); + await video.loadComplete; + + t.ok(video.api, 'has a player once loaded'); + t.ok(video.shadowRoot.querySelector('iframe'), 'has an iframe once loaded'); + + video.remove(); + + t.equal(video.api, null, 'the player reference is released on disconnect'); + t.equal(video.isLoaded, false, 'the element is no longer marked loaded'); + t.equal( + video.shadowRoot.querySelector('iframe'), + null, + 'destroy() removed the iframe from the shadow root' + ); +}); + +test('creates a new player when reconnected', async function (t) { + const video = await createVideoElement(); + await video.loadComplete; + + const firstApi = video.api; + video.remove(); + document.body.append(video); + + await video.loadComplete; + + t.ok(video.api, 'has a player again after reconnecting'); + t.ok(video.api !== firstApi, 'a new player was created, not the destroyed one'); + t.ok(video.shadowRoot.querySelector('iframe'), 'the iframe was rebuilt'); +}); + +test('disconnecting before the player is ready does not throw', async function (t) { + const video = await createVideoElement(); + // Do not await loadComplete: the API may still be loading. + video.remove(); + + t.equal(video.api, null, 'no player is left behind'); + t.ok(true, 'disconnecting mid-load did not throw'); +}); + function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/packages/youtube-video-element/youtube-video-element.js b/packages/youtube-video-element/youtube-video-element.js index 75958db..b034f33 100644 --- a/packages/youtube-video-element/youtube-video-element.js +++ b/packages/youtube-video-element/youtube-video-element.js @@ -147,6 +147,9 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement loadComplete = new PublicPromise(); #loadRequested; #hasLoaded; + #wasDisconnected = false; + #loadId = 0; + #timers = []; #readyState = 0; #seeking = false; #seekComplete; @@ -172,6 +175,12 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement async load() { if (this.#loadRequested) return; + // Identifies this load attempt. `load()` awaits the API script, so the + // element can be disconnected (or asked to load again) while this one is + // still in flight; those bump #loadId and this attempt bails out below + // instead of attaching a player nothing will ever destroy. + const loadId = ++this.#loadId; + if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } @@ -193,12 +202,13 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement let oldApi = this.api; this.api = null; + // A new player is always constructed below, so the previous one is never + // reused. Release it rather than just dropping the reference, otherwise it + // stays in the API's registries holding its (now discarded) iframe. + // Removes the