diff --git a/packages/ui/src/custom/media-player/media-player.svelte b/packages/ui/src/custom/media-player/media-player.svelte
index 62496d7b..d7d099fb 100644
--- a/packages/ui/src/custom/media-player/media-player.svelte
+++ b/packages/ui/src/custom/media-player/media-player.svelte
@@ -2,6 +2,7 @@
import type { MediaPlayerOptions, VideoSource } from './types';
import PlyrPlayer from './players/plyr-player.svelte';
import MusePlayer from './players/muse-player.svelte';
+ import { formatYoutubeEmbedUrl } from './utils';
interface Props {
source: VideoSource;
@@ -26,11 +27,13 @@
const tracks = $derived(source.tracks ?? []);
const isMuse = $derived.by(() => source.type === 'muse' && source.metadata?.svid);
const isGoogleDrive = $derived(source.type === 'google_drive');
+ const isYouTube = $derived(source.type === 'youtube');
const poster = $derived(source.type === 'upload' ? source.metadata?.thumbnailUrl : undefined);
const iframeTitle = $derived(source.metadata?.title?.trim() || 'Video');
const iframeMaxHeight = $derived(options.maxHeight ?? '400px');
const iframeWidth = $derived(options.width ?? '100%');
+ const youtubeEmbedUrl = $derived(isYouTube ? formatYoutubeEmbedUrl(source.url) : '');
@@ -51,6 +54,23 @@
allowfullscreen
>
+ {:else if isYouTube}
+
+
+
+
{:else}
{/if}
diff --git a/packages/ui/src/custom/media-player/players/plyr-player.svelte b/packages/ui/src/custom/media-player/players/plyr-player.svelte
index adb3ce8d..bf93725d 100644
--- a/packages/ui/src/custom/media-player/players/plyr-player.svelte
+++ b/packages/ui/src/custom/media-player/players/plyr-player.svelte
@@ -42,6 +42,8 @@
let hlsPlyrGeneration = 0;
let plyrConstructInFlight = false;
let hasFiredFirstPlay = false;
+ let isMounted = false;
+ let youtubeInitGeneration = 0;
interface HlsQualityConfig {
default: number;
@@ -193,7 +195,7 @@
try {
// Recheck after the dynamic import resolves — the component may have
// unmounted (or the videoElement bind nulled out) while we awaited.
- if (!videoElement || playerInstance) return;
+ if (!isMounted || !videoElement || playerInstance) return;
mediaErrorCleanup = attachMediaErrorHandler(videoElement);
@@ -257,23 +259,43 @@
});
}
+ async function constructPlyrForYouTubeEmbed(): Promise {
+ if (!isMounted || !containerElement || !youtubeVideoId || playerInstance || plyrConstructInFlight) return;
+
+ const initGeneration = youtubeInitGeneration;
+ plyrConstructInFlight = true;
+
+ const PlyrModule = await import('plyr');
+ const PlyrConstructor = PlyrModule.default;
+ const controls = showControls ? PLYR_DEFAULT_CONTROLS : [];
+
+ try {
+ if (!isMounted || initGeneration !== youtubeInitGeneration || !containerElement || playerInstance) {
+ return;
+ }
+
+ playerInstance = new PlyrConstructor(containerElement, {
+ controls,
+ autoplay,
+ ratio: '16:9',
+ iconUrl: '/plyr.svg',
+ iconPrefix: 'plyr'
+ });
+ timeupdateCleanup = attachTimeUpdates(playerInstance);
+ firstPlayCleanup = attachFirstPlay(playerInstance);
+ plyrErrorCleanup = attachPlyrErrorHandler(playerInstance);
+ options.onPlayerReady?.(playerInstance);
+ } finally {
+ plyrConstructInFlight = false;
+ }
+ }
+
onMount(() => {
+ isMounted = true;
+
void (async () => {
if (isYouTube && youtubeVideoId && containerElement) {
- const PlyrModule = await import('plyr');
- const PlyrConstructor = PlyrModule.default;
- const controls = showControls ? PLYR_DEFAULT_CONTROLS : [];
- playerInstance = new PlyrConstructor(containerElement, {
- controls,
- autoplay,
- ratio: '16:9',
- iconUrl: '/plyr.svg',
- iconPrefix: 'plyr'
- });
- timeupdateCleanup = attachTimeUpdates(playerInstance);
- firstPlayCleanup = attachFirstPlay(playerInstance);
- plyrErrorCleanup = attachPlyrErrorHandler(playerInstance);
- options.onPlayerReady?.(playerInstance);
+ await constructPlyrForYouTubeEmbed();
return;
}
@@ -448,6 +470,8 @@
});
onDestroy(() => {
+ isMounted = false;
+ youtubeInitGeneration += 1;
hlsLoadGeneration += 1;
hlsPlyrGeneration = 0;
destroyHlsInstance();
@@ -463,6 +487,10 @@
}
teardownPlyrInstance();
+
+ if (isYouTube && containerElement) {
+ containerElement.replaceChildren();
+ }
});