@flamingo-stack/openframe-frontend-core 0.0.581 → 0.0.582-1960.5386.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/{chunk-R5JVRFSW.js → chunk-4KWAHIYA.js} +3 -3
  2. package/dist/{chunk-KAEVE7W2.js → chunk-646JEAY4.js} +2 -2
  3. package/dist/{chunk-GAHKWML7.js → chunk-6EUYXDV3.js} +6 -6
  4. package/dist/{chunk-HVSZYUGP.js → chunk-7HHRJTIY.js} +5 -5
  5. package/dist/{chunk-JW53WPB5.js → chunk-A3SVEK7A.js} +3 -3
  6. package/dist/{chunk-6U45VX5J.cjs → chunk-A5VD6R7N.cjs} +43 -43
  7. package/dist/{chunk-6U45VX5J.cjs.map → chunk-A5VD6R7N.cjs.map} +1 -1
  8. package/dist/{chunk-BTKMTVN5.cjs → chunk-ALSLHGBH.cjs} +4 -4
  9. package/dist/{chunk-BTKMTVN5.cjs.map → chunk-ALSLHGBH.cjs.map} +1 -1
  10. package/dist/{chunk-SEZ25E22.cjs → chunk-BQPCCBGO.cjs} +10 -10
  11. package/dist/{chunk-SEZ25E22.cjs.map → chunk-BQPCCBGO.cjs.map} +1 -1
  12. package/dist/{chunk-LWDIDSCF.js → chunk-BUOYMIFJ.js} +3 -3
  13. package/dist/{chunk-3GXNCJZI.js → chunk-CR7GPMCM.js} +3 -3
  14. package/dist/{chunk-U3W2ZVN4.cjs → chunk-DGZPX2G3.cjs} +159 -159
  15. package/dist/{chunk-U3W2ZVN4.cjs.map → chunk-DGZPX2G3.cjs.map} +1 -1
  16. package/dist/{chunk-PBRXT7TD.cjs → chunk-DQVM4FTL.cjs} +10 -10
  17. package/dist/{chunk-PBRXT7TD.cjs.map → chunk-DQVM4FTL.cjs.map} +1 -1
  18. package/dist/{chunk-HJVKPG55.js → chunk-F227VMPL.js} +125 -15
  19. package/dist/{chunk-HJVKPG55.js.map → chunk-F227VMPL.js.map} +1 -1
  20. package/dist/{chunk-EZVABFEO.js → chunk-FRWQ67UM.js} +2 -2
  21. package/dist/{chunk-DSPQVZFE.cjs → chunk-FYVL6A4Z.cjs} +24 -24
  22. package/dist/{chunk-DSPQVZFE.cjs.map → chunk-FYVL6A4Z.cjs.map} +1 -1
  23. package/dist/{chunk-VXUUSLD5.js → chunk-GD3OAM4T.js} +2 -2
  24. package/dist/{chunk-ESWXXWCL.js → chunk-HQF7VE7B.js} +2 -2
  25. package/dist/{chunk-VDRCKHML.cjs → chunk-J5I3TKAH.cjs} +3 -3
  26. package/dist/{chunk-VDRCKHML.cjs.map → chunk-J5I3TKAH.cjs.map} +1 -1
  27. package/dist/{chunk-GUC4JK6O.cjs → chunk-K5SCFOD5.cjs} +9 -9
  28. package/dist/{chunk-GUC4JK6O.cjs.map → chunk-K5SCFOD5.cjs.map} +1 -1
  29. package/dist/{chunk-UKHM2S6I.cjs → chunk-KAOXOYRT.cjs} +15 -15
  30. package/dist/{chunk-UKHM2S6I.cjs.map → chunk-KAOXOYRT.cjs.map} +1 -1
  31. package/dist/{chunk-IHFQ5VVR.js → chunk-KBOBPKMU.js} +5 -5
  32. package/dist/{chunk-GMQEHDN2.cjs → chunk-L4E2TOSF.cjs} +5 -5
  33. package/dist/{chunk-GMQEHDN2.cjs.map → chunk-L4E2TOSF.cjs.map} +1 -1
  34. package/dist/{chunk-YU2UJPAT.cjs → chunk-LOJ5KQ6B.cjs} +466 -356
  35. package/dist/chunk-LOJ5KQ6B.cjs.map +1 -0
  36. package/dist/{chunk-M4KQWGEL.cjs → chunk-ORQQZQBB.cjs} +32 -32
  37. package/dist/{chunk-M4KQWGEL.cjs.map → chunk-ORQQZQBB.cjs.map} +1 -1
  38. package/dist/{chunk-ZEZMGXYV.js → chunk-QDSVTIO5.js} +5 -5
  39. package/dist/{chunk-KNBXZ22O.cjs → chunk-R3J2ERH2.cjs} +3 -3
  40. package/dist/{chunk-KNBXZ22O.cjs.map → chunk-R3J2ERH2.cjs.map} +1 -1
  41. package/dist/{chunk-WHFEG6UU.cjs → chunk-RMOUILHT.cjs} +22 -22
  42. package/dist/{chunk-WHFEG6UU.cjs.map → chunk-RMOUILHT.cjs.map} +1 -1
  43. package/dist/{chunk-OKA3DJJ3.js → chunk-SKGMU7TZ.js} +2 -2
  44. package/dist/{chunk-7CP5KY5W.js → chunk-SVQVAGZK.js} +3 -3
  45. package/dist/{chunk-W2TXSZTM.cjs → chunk-T32XVOFP.cjs} +21 -21
  46. package/dist/{chunk-W2TXSZTM.cjs.map → chunk-T32XVOFP.cjs.map} +1 -1
  47. package/dist/{chunk-GVM37TRU.js → chunk-U2ZRM3YB.js} +4 -4
  48. package/dist/{chunk-AIALG25H.cjs → chunk-WQ236GO3.cjs} +69 -69
  49. package/dist/{chunk-AIALG25H.cjs.map → chunk-WQ236GO3.cjs.map} +1 -1
  50. package/dist/components/case-studies/index.cjs +9 -9
  51. package/dist/components/case-studies/index.js +3 -3
  52. package/dist/components/chat/index.cjs +2 -2
  53. package/dist/components/chat/index.js +1 -1
  54. package/dist/components/contact/index.cjs +4 -4
  55. package/dist/components/contact/index.js +3 -3
  56. package/dist/components/docs/index.cjs +7 -7
  57. package/dist/components/docs/index.js +6 -6
  58. package/dist/components/embeds/index.cjs +4 -4
  59. package/dist/components/embeds/index.js +3 -3
  60. package/dist/components/faq/index.cjs +4 -4
  61. package/dist/components/faq/index.js +3 -3
  62. package/dist/components/features/index.cjs +5 -3
  63. package/dist/components/features/index.cjs.map +1 -1
  64. package/dist/components/features/index.js +4 -2
  65. package/dist/components/features/video.d.ts +12 -0
  66. package/dist/components/features/video.d.ts.map +1 -1
  67. package/dist/components/help-center-pages/index.cjs +23 -23
  68. package/dist/components/help-center-pages/index.js +14 -14
  69. package/dist/components/index.cjs +123 -121
  70. package/dist/components/index.cjs.map +1 -1
  71. package/dist/components/index.js +15 -13
  72. package/dist/components/index.js.map +1 -1
  73. package/dist/components/meeting-scheduler/index.cjs +21 -21
  74. package/dist/components/meeting-scheduler/index.js +2 -2
  75. package/dist/components/navigation/index.cjs +6 -6
  76. package/dist/components/navigation/index.js +5 -5
  77. package/dist/components/onboarding-guides/index.cjs +6 -6
  78. package/dist/components/onboarding-guides/index.js +5 -5
  79. package/dist/components/tickets/index.cjs +7 -7
  80. package/dist/components/tickets/index.js +6 -6
  81. package/dist/components/ui/index.cjs +3 -3
  82. package/dist/components/ui/index.js +2 -2
  83. package/dist/index.cjs +9 -7
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.js +8 -6
  86. package/dist/index.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/components/features/__tests__/video-playback-failure.test.tsx +250 -0
  89. package/src/components/features/video.tsx +198 -16
  90. package/dist/chunk-YU2UJPAT.cjs.map +0 -1
  91. /package/dist/{chunk-R5JVRFSW.js.map → chunk-4KWAHIYA.js.map} +0 -0
  92. /package/dist/{chunk-KAEVE7W2.js.map → chunk-646JEAY4.js.map} +0 -0
  93. /package/dist/{chunk-GAHKWML7.js.map → chunk-6EUYXDV3.js.map} +0 -0
  94. /package/dist/{chunk-HVSZYUGP.js.map → chunk-7HHRJTIY.js.map} +0 -0
  95. /package/dist/{chunk-JW53WPB5.js.map → chunk-A3SVEK7A.js.map} +0 -0
  96. /package/dist/{chunk-LWDIDSCF.js.map → chunk-BUOYMIFJ.js.map} +0 -0
  97. /package/dist/{chunk-3GXNCJZI.js.map → chunk-CR7GPMCM.js.map} +0 -0
  98. /package/dist/{chunk-EZVABFEO.js.map → chunk-FRWQ67UM.js.map} +0 -0
  99. /package/dist/{chunk-VXUUSLD5.js.map → chunk-GD3OAM4T.js.map} +0 -0
  100. /package/dist/{chunk-ESWXXWCL.js.map → chunk-HQF7VE7B.js.map} +0 -0
  101. /package/dist/{chunk-IHFQ5VVR.js.map → chunk-KBOBPKMU.js.map} +0 -0
  102. /package/dist/{chunk-ZEZMGXYV.js.map → chunk-QDSVTIO5.js.map} +0 -0
  103. /package/dist/{chunk-OKA3DJJ3.js.map → chunk-SKGMU7TZ.js.map} +0 -0
  104. /package/dist/{chunk-7CP5KY5W.js.map → chunk-SVQVAGZK.js.map} +0 -0
  105. /package/dist/{chunk-GVM37TRU.js.map → chunk-U2ZRM3YB.js.map} +0 -0
@@ -35,6 +35,7 @@ import type React from 'react';
35
35
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
36
36
  import { useAuthedAssetSrc } from '../../hooks/use-authed-asset-src';
37
37
  import { fetchPriorityProp } from '../../utils/fetch-priority';
38
+ import { Button } from '../ui/button';
38
39
  import { useIosNativeVideoFullscreen } from './use-ios-native-video-fullscreen';
39
40
  import { saveDataEnabled } from './use-video-warmup';
40
41
  import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
@@ -280,6 +281,76 @@ export function extractYouTubeId(url: string): string | null {
280
281
  return m ? m[1] : null;
281
282
  }
282
283
 
284
+ // =============================================================================
285
+ // Playback-failure handling
286
+ // =============================================================================
287
+ //
288
+ // A stalled or errored player leaves the user on an endless spinner with no way
289
+ // out. Every failure surfaces a retry (`VideoErrorOverlay`) and is reported on
290
+ // a `window` CustomEvent the host subscribes to — no vendor analytics global is
291
+ // touched from here; this package ships to consumers that load different (or
292
+ // no) analytics.
293
+
294
+ export type VideoFailureKind = 'file' | 'youtube';
295
+ /** `stall`: buffering that never cleared while playback was expected. */
296
+ export type VideoFailureReason = 'error' | 'stall';
297
+
298
+ export interface VideoPlaybackFailureDetail {
299
+ kind: VideoFailureKind;
300
+ /** File URL or YouTube video id — enough to group failures by source. */
301
+ src: string;
302
+ reason: VideoFailureReason;
303
+ /** YouTube IFrame API error code, when the embed reported one. */
304
+ errorCode?: number;
305
+ }
306
+
307
+ export const VIDEO_PLAYBACK_FAILED_EVENT = 'flamingo:video-playback-failed';
308
+
309
+ function reportPlaybackFailure(detail: VideoPlaybackFailureDetail): void {
310
+ if (typeof window === 'undefined') return;
311
+ try {
312
+ window.dispatchEvent(new CustomEvent(VIDEO_PLAYBACK_FAILED_EVENT, { detail }));
313
+ } catch {
314
+ // Analytics must never break playback recovery.
315
+ }
316
+ }
317
+
318
+ // Buffering longer than this while playback is expected reads as "broken", not
319
+ // "loading" — surface a retry instead of spinning forever. This is what an
320
+ // `error` callback alone misses: a video that played, then stalled mid-way.
321
+ const VIDEO_STALL_TIMEOUT_MS = 15_000;
322
+
323
+ /** `watchOnYouTubeId` adds the one recovery that survives an embed the owner
324
+ * disabled: opening the video on YouTube itself. */
325
+ function VideoErrorOverlay({
326
+ onRetry,
327
+ watchOnYouTubeId,
328
+ }: {
329
+ onRetry: () => void;
330
+ watchOnYouTubeId?: string;
331
+ }): React.ReactElement {
332
+ return (
333
+ <div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-[var(--spacing-system-sf)] bg-ods-overlay p-[var(--spacing-system-lf)] text-center">
334
+ <p className="text-ods-text-secondary text-h6">This video failed to play.</p>
335
+ <div className="flex items-center gap-[var(--spacing-system-sf)]">
336
+ <Button variant="outline" size="small" onClick={onRetry}>
337
+ Try again
338
+ </Button>
339
+ {watchOnYouTubeId ? (
340
+ <Button
341
+ variant="transparent"
342
+ size="small"
343
+ href={`https://www.youtube.com/watch?v=${watchOnYouTubeId}`}
344
+ openInNewTab
345
+ >
346
+ Watch on YouTube
347
+ </Button>
348
+ ) : null}
349
+ </div>
350
+ </div>
351
+ );
352
+ }
353
+
283
354
  // =============================================================================
284
355
  // Props
285
356
  // =============================================================================
@@ -690,11 +761,13 @@ function FilePlayer({
690
761
  const hoverPlayerRef = useRef<{
691
762
  play?: () => Promise<void> | void;
692
763
  pause?: () => void;
764
+ load?: () => void;
693
765
  muted?: boolean;
694
766
  volume?: number;
695
767
  currentTime?: number;
696
768
  duration?: number;
697
769
  paused?: boolean;
770
+ ended?: boolean;
698
771
  addEventListener?: (type: string, listener: () => void) => void;
699
772
  removeEventListener?: (type: string, listener: () => void) => void;
700
773
  } | null>(null);
@@ -1100,9 +1173,93 @@ function FilePlayer({
1100
1173
  );
1101
1174
  }
1102
1175
 
1176
+ // Only chrome-bearing surfaces (a user actively watching) monitor health —
1177
+ // decorative first-frame and hover previews pass `chromeless` and stay silent.
1178
+ const monitorHealth = !chromeless;
1179
+ const [playbackFailed, setPlaybackFailed] = useState(false);
1180
+ const stallTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1181
+ const clearStallTimer = useCallback(() => {
1182
+ if (stallTimerRef.current !== null) {
1183
+ clearTimeout(stallTimerRef.current);
1184
+ stallTimerRef.current = null;
1185
+ }
1186
+ }, []);
1187
+ useEffect(() => clearStallTimer, [clearStallTimer]);
1188
+
1189
+ const failPlayback = useCallback(
1190
+ (reason: VideoFailureReason) => {
1191
+ clearStallTimer();
1192
+ setPlaybackFailed(true);
1193
+ reportPlaybackFailure({ kind: 'file', src: url, reason });
1194
+ },
1195
+ [clearStallTimer, url],
1196
+ );
1197
+
1198
+ // A manually paused or finished video is never a stall.
1199
+ const armStallTimer = useCallback(() => {
1200
+ if (!monitorHealth) return;
1201
+ const el = hoverPlayerRef.current;
1202
+ if (!el || el.paused || el.ended) return;
1203
+ if (stallTimerRef.current !== null) return;
1204
+ stallTimerRef.current = setTimeout(() => {
1205
+ stallTimerRef.current = null;
1206
+ failPlayback('stall');
1207
+ }, VIDEO_STALL_TIMEOUT_MS);
1208
+ }, [monitorHealth, failPlayback]);
1209
+
1210
+ // Progress means the player is alive — a stall that self-heals clears its own
1211
+ // overlay.
1212
+ const handlePlaybackProgress = useCallback(() => {
1213
+ clearStallTimer();
1214
+ setPlaybackFailed(false);
1215
+ }, [clearStallTimer]);
1216
+
1217
+ const handleMediaError = useCallback(() => {
1218
+ if (!monitorHealth) return;
1219
+ failPlayback('error');
1220
+ }, [monitorHealth, failPlayback]);
1221
+
1222
+ // `load()` re-inits the HLS pipeline (`play()` alone can't) but rewinds to
1223
+ // zero — stash the position and seek back on `loadedmetadata`, since an
1224
+ // earlier assignment is dropped while the fresh source has no seekable range.
1225
+ const retryPlayback = useCallback(() => {
1226
+ setPlaybackFailed(false);
1227
+ clearStallTimer();
1228
+ const el = hoverPlayerRef.current;
1229
+ if (!el) return;
1230
+ const resumeAt = typeof el.currentTime === 'number' ? el.currentTime : 0;
1231
+ try {
1232
+ el.load?.();
1233
+ if (resumeAt > 0 && el.addEventListener && el.removeEventListener) {
1234
+ const seekBack = () => {
1235
+ el.removeEventListener?.('loadedmetadata', seekBack);
1236
+ try {
1237
+ el.currentTime = resumeAt;
1238
+ } catch {
1239
+ /* source shorter than the stashed position — start from the top */
1240
+ }
1241
+ };
1242
+ el.addEventListener('loadedmetadata', seekBack);
1243
+ }
1244
+ (el.play?.() as Promise<void> | undefined)?.catch?.(() => {
1245
+ /* a re-rejected play surfaces again via the error/stall handlers */
1246
+ });
1247
+ } catch {
1248
+ /* element torn down */
1249
+ }
1250
+ }, [clearStallTimer]);
1251
+
1252
+ const errorOverlay = monitorHealth && playbackFailed ? <VideoErrorOverlay onRetry={retryPlayback} /> : null;
1253
+
1103
1254
  const player = (
1104
1255
  <MuxPlayer
1105
1256
  ref={hoverPlayerRef as React.Ref<never>}
1257
+ onError={handleMediaError}
1258
+ onWaiting={armStallTimer}
1259
+ onStalled={armStallTimer}
1260
+ onPlaying={handlePlaybackProgress}
1261
+ onTimeUpdate={handlePlaybackProgress}
1262
+ onPause={clearStallTimer}
1106
1263
  src={url}
1107
1264
  poster={poster || undefined}
1108
1265
  streamType="on-demand"
@@ -1187,26 +1344,18 @@ function FilePlayer({
1187
1344
  <div className="relative h-full w-full" onPointerEnter={handleHoverEnter} onPointerLeave={handleHoverLeave}>
1188
1345
  {player}
1189
1346
  {unmuteBadge}
1347
+ {errorOverlay}
1190
1348
  </div>
1191
1349
  );
1192
1350
  }
1193
- if (hoverControlled) {
1194
- return (
1195
- <div className="relative h-full w-full">
1196
- {player}
1197
- {unmuteBadge}
1198
- </div>
1199
- );
1200
- }
1201
- // Handoff surfaces (autoPlayUnmuted / startMuted) need the relative wrapper so
1202
- // the internal center unmute badge can dock — the bare branch has none. Hosts
1203
- // that render their OWN control pass hideMutedBadge (unmuteBadge is null then,
1204
- // but the wrapper is harmless).
1205
- if (autoPlayUnmuted || startMuted) {
1351
+ // A relative wrapper so the unmute badge and the error overlay can dock. The
1352
+ // bare `player` return is left only for chromeless previews (never monitored).
1353
+ if (hoverControlled || autoPlayUnmuted || startMuted || monitorHealth) {
1206
1354
  return (
1207
1355
  <div className="relative h-full w-full">
1208
1356
  {player}
1209
1357
  {unmuteBadge}
1358
+ {errorOverlay}
1210
1359
  </div>
1211
1360
  );
1212
1361
  }
@@ -1288,6 +1437,9 @@ const YT_PLAYING_BLUR_DELAY_MS = 1000;
1288
1437
  interface YouTubeInfoDeliveryMessage {
1289
1438
  event?: string;
1290
1439
  info?: { playerState?: number };
1440
+ /** `onError` carries its code as a bare number in `info`, not an object:
1441
+ * `{"event":"onError","info":150}` (101/150 = embedding disabled). */
1442
+ errorCode?: number;
1291
1443
  }
1292
1444
 
1293
1445
  /**
@@ -1307,6 +1459,7 @@ function toYouTubeMessage(parsed: unknown): YouTubeInfoDeliveryMessage | null {
1307
1459
  return {
1308
1460
  event: typeof event === 'string' ? event : undefined,
1309
1461
  info: { playerState: typeof playerState === 'number' ? playerState : undefined },
1462
+ errorCode: typeof info === 'number' ? info : undefined,
1310
1463
  };
1311
1464
  }
1312
1465
 
@@ -1320,8 +1473,18 @@ function YouTubeFacadeInner({
1320
1473
  suspended,
1321
1474
  }: YouTubeFacadeInnerProps): React.ReactElement {
1322
1475
  const [activated, setActivated] = useState(Boolean(autoActivate));
1476
+ // The embed reported a hard player error (see the `onError` branch below).
1477
+ // Retry remounts the iframe via `reloadNonce` — a fresh element, since there
1478
+ // is no way to re-init a cross-origin player from outside.
1479
+ const [failed, setFailed] = useState(false);
1480
+ const [reloadNonce, setReloadNonce] = useState(0);
1323
1481
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
1324
1482
 
1483
+ const retry = useCallback(() => {
1484
+ setFailed(false);
1485
+ setReloadNonce(n => n + 1);
1486
+ }, []);
1487
+
1325
1488
  // Embed URL + poster URLs only change when `videoId` or `minimalControls`
1326
1489
  // do — memoize so we don't rebuild URLSearchParams on every render.
1327
1490
  //
@@ -1414,7 +1577,7 @@ function YouTubeFacadeInner({
1414
1577
  // end-of-video (ENDED).
1415
1578
  // ---------------------------------------------------------------------------
1416
1579
  useEffect(() => {
1417
- if (!activated) return undefined;
1580
+ if (!activated || failed) return undefined;
1418
1581
  const iframe = iframeRef.current;
1419
1582
  if (!iframe) return undefined;
1420
1583
 
@@ -1427,6 +1590,10 @@ function YouTubeFacadeInner({
1427
1590
 
1428
1591
  let blurTimer: ReturnType<typeof setTimeout> | null = null;
1429
1592
 
1593
+ // No "never reached PLAYING within N seconds" watchdog on purpose: an embed
1594
+ // whose unmuted autoplay Safari/iOS blocked, or one whose jsapi channel is
1595
+ // dead, is healthy and silent — a timeout would tear those down, and retry
1596
+ // would re-arm it. Only an explicit `onError` is a definitive failure.
1430
1597
  function handleMessage(event: MessageEvent) {
1431
1598
  if (event.origin !== YT_NOCOOKIE_ORIGIN) return;
1432
1599
  if (typeof event.data !== 'string') return;
@@ -1436,7 +1603,13 @@ function YouTubeFacadeInner({
1436
1603
  } catch {
1437
1604
  return;
1438
1605
  }
1439
- if (!payload || payload.event !== 'infoDelivery') return;
1606
+ if (!payload) return;
1607
+ if (payload.event === 'onError') {
1608
+ setFailed(true);
1609
+ reportPlaybackFailure({ kind: 'youtube', src: videoId, reason: 'error', errorCode: payload.errorCode });
1610
+ return;
1611
+ }
1612
+ if (payload.event !== 'infoDelivery') return;
1440
1613
  const state = payload.info?.playerState;
1441
1614
  if (typeof state !== 'number') return;
1442
1615
 
@@ -1459,7 +1632,7 @@ function YouTubeFacadeInner({
1459
1632
  window.removeEventListener('message', handleMessage);
1460
1633
  if (blurTimer !== null) clearTimeout(blurTimer);
1461
1634
  };
1462
- }, [activated]);
1635
+ }, [activated, failed, reloadNonce, videoId]);
1463
1636
 
1464
1637
  // Close-side pause: a closing dialog flips `suspended` false→true. Post the
1465
1638
  // pauseVideo command over the same enablejsapi channel so the iframe stops
@@ -1482,10 +1655,19 @@ function YouTubeFacadeInner({
1482
1655
  const wrapperClass = `relative w-full ${className ?? ''}`;
1483
1656
  const wrapperStyle = { paddingBottom: '56.25%' as const };
1484
1657
 
1658
+ if (activated && failed) {
1659
+ return (
1660
+ <div className={wrapperClass} style={wrapperStyle}>
1661
+ <VideoErrorOverlay onRetry={retry} watchOnYouTubeId={videoId} />
1662
+ </div>
1663
+ );
1664
+ }
1665
+
1485
1666
  if (activated) {
1486
1667
  return (
1487
1668
  <div className={wrapperClass} style={wrapperStyle}>
1488
1669
  <iframe
1670
+ key={reloadNonce}
1489
1671
  ref={iframeRef}
1490
1672
  src={embedUrl}
1491
1673
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"