@@ -109,6 +109,10 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
109109 #videoWidth = NaN ;
110110 #videoHeight = NaN ;
111111 #config = null ;
112+ /** Distinguishes a remount from SSR hydration.
113+ * See load()
114+ */
115+ #wasDisconnected = false ;
112116
113117 constructor ( ) {
114118 super ( ) ;
@@ -136,17 +140,13 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
136140 }
137141
138142 set config ( value ) {
143+ if ( JSON . stringify ( this . #config) === JSON . stringify ( value ) ) return ;
139144 this . #config = value ;
145+ this . load ( ) ;
140146 }
141147
142148 async load ( ) {
143149 if ( this . #loadRequested) return ;
144-
145- const isFirstLoad = ! this . #hasLoaded;
146-
147- if ( this . #hasLoaded) this . loadComplete = new PublicPromise ( ) ;
148- this . #hasLoaded = true ;
149-
150150 // Wait 1 tick to allow other attributes to be set.
151151 await ( this . #loadRequested = Promise . resolve ( ) ) ;
152152 this . #loadRequested = null ;
@@ -169,9 +169,15 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
169169 this . api = null ;
170170
171171 if ( ! this . src ) {
172+ // Nothing to load. Leave loadComplete and #hasLoaded untouched so
173+ // callers awaiting the existing loadComplete aren't orphaned if a
174+ // later load() (e.g. triggered by a subsequent src) replaces it.
172175 return ;
173176 }
174177
178+ if ( this . #hasLoaded) this . loadComplete = new PublicPromise ( ) ;
179+ this . #hasLoaded = true ;
180+
175181 this . dispatchEvent ( new Event ( 'loadstart' ) ) ;
176182
177183 // https://developer.vimeo.com/player/sdk/embed
@@ -187,50 +193,86 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
187193 ...this . #config,
188194 } ;
189195
190- const onLoaded = async ( ) => {
191- this . #readyState = 1 ; // HTMLMediaElement.HAVE_METADATA
192- this . dispatchEvent ( new Event ( 'loadedmetadata' ) ) ;
193-
194- if ( this . api ) {
195- this . #muted = await this . api . getMuted ( ) ;
196- this . #volume = await this . api . getVolume ( ) ;
197- this . dispatchEvent ( new Event ( 'volumechange' ) ) ;
198-
199- this . #duration = await this . api . getDuration ( ) ;
200- this . dispatchEvent ( new Event ( 'durationchange' ) ) ;
201- }
202-
203- this . dispatchEvent ( new Event ( 'loadcomplete' ) ) ;
204- this . loadComplete . resolve ( ) ;
205- } ;
206-
207196 if ( this . #isInit) {
208197 this . api = oldApi ;
209198 await this . api . loadVideo ( {
210199 ...options ,
211200 url : this . src ,
212201 } ) ;
213- await onLoaded ( ) ;
202+ await this . # onLoaded( ) ;
214203 await this . loadComplete ;
215204 return ;
216205 }
217206
218207 this . #isInit = true ;
219208
220- let iframe = this . shadowRoot ?. querySelector ( 'iframe' ) ;
209+ /*
210+ * Decide whether to build the iframe or adopt an existing one:
211+ * - First client mount or remount after disconnect: build, so the iframe
212+ * URL reflects current src and config. The Vimeo SDK wraps an existing
213+ * iframe as-is and won't update its URL.
214+ * - SSR declarative shadow DOM hydration: adopt the existing iframe. Its
215+ * URL is already correct, and rebuilding would destroy the in-flight
216+ * request and cause a visible stutter. Recover config from the
217+ * data-config attribute so element state matches the DOM.
218+ */
219+ if ( ! this . shadowRoot ) this . attachShadow ( { mode : 'open' } ) ;
220+
221+ const existingIframe = this . shadowRoot . querySelector ( 'iframe' ) ;
222+ const isSsrHydration = existingIframe && ! this . #wasDisconnected;
223+
224+ if ( isSsrHydration ) {
225+ if ( ! this . #config) {
226+ this . #config = JSON . parse ( existingIframe . getAttribute ( 'data-config' ) || '{}' ) ;
227+ }
228+ } else {
229+ this . shadowRoot . innerHTML = getTemplateHTML ( namedNodeMapToObject ( this . attributes ) , this ) ;
230+ }
231+ this . #wasDisconnected = false ;
221232
222- if ( isFirstLoad && iframe ) {
223- this . #config = JSON . parse ( iframe . getAttribute ( 'data-config' ) || '{}' ) ;
233+ oldApi ?. destroy ?. ( ) ;
234+ const iframe = this . shadowRoot . querySelector ( 'iframe' ) ;
235+ this . api = new VimeoPlayerAPI ( iframe ) ;
236+ this . #setupApiListeners( ) ;
237+ await this . loadComplete ;
238+ }
239+
240+ connectedCallback ( ) {
241+ if ( this . #wasDisconnected) {
242+ this . load ( ) ;
224243 }
244+ super . connectedCallback ?. ( ) ;
245+ }
225246
226- if ( ! this . shadowRoot ) {
227- this . attachShadow ( { mode : 'open' } ) ;
228- this . shadowRoot . innerHTML = getTemplateHTML ( namedNodeMapToObject ( this . attributes ) , this ) ;
229- iframe = this . shadowRoot . querySelector ( 'iframe' ) ;
247+ disconnectedCallback ( ) {
248+ this . #wasDisconnected = true ;
249+ this . #loadRequested = null ;
250+ this . #hasLoaded = null ;
251+ this . #isInit = null ;
252+ this . loadComplete = new PublicPromise ( ) ;
253+ this . api ?. destroy ?. ( ) ;
254+ this . api = null ;
255+ super . disconnectedCallback ?. ( ) ;
256+ }
257+
258+ #onLoaded = async ( ) => {
259+ this . #readyState = 1 ; // HTMLMediaElement.HAVE_METADATA
260+ this . dispatchEvent ( new Event ( 'loadedmetadata' ) ) ;
261+
262+ if ( this . api ) {
263+ this . #muted = await this . api . getMuted ( ) ;
264+ this . #volume = await this . api . getVolume ( ) ;
265+ this . dispatchEvent ( new Event ( 'volumechange' ) ) ;
266+
267+ this . #duration = await this . api . getDuration ( ) ;
268+ this . dispatchEvent ( new Event ( 'durationchange' ) ) ;
230269 }
231270
232- this . api = new VimeoPlayerAPI ( iframe ) ;
271+ this . dispatchEvent ( new Event ( 'loadcomplete' ) ) ;
272+ this . loadComplete . resolve ( ) ;
273+ } ;
233274
275+ #setupApiListeners( ) {
234276 const textTracksVideo = document . createElement ( 'video' ) ;
235277 this . textTracks = textTracksVideo . textTracks ;
236278 this . api . getTextTracks ( ) . then ( ( vimeoTracks ) => {
@@ -249,7 +291,7 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
249291
250292 const onceLoaded = ( ) => {
251293 this . api . off ( 'loaded' , onceLoaded ) ;
252- onLoaded ( ) ;
294+ this . # onLoaded( ) ;
253295 } ;
254296 this . api . on ( 'loaded' , onceLoaded ) ;
255297
@@ -333,8 +375,6 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
333375 this . #videoHeight = videoHeight ;
334376 this . dispatchEvent ( new Event ( 'resize' ) ) ;
335377 } ) ;
336-
337- await this . loadComplete ;
338378 }
339379
340380 async attributeChangedCallback ( attrName , oldValue , newValue ) {
0 commit comments