From a70f8c5287ee6273866a20723f919a75641c799f Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sun, 28 Jun 2026 13:15:37 +0100 Subject: [PATCH] fix: hls session ending and user can't watch video --- apps/api/src/routes/org-site/public-course.ts | 6 ++++- apps/api/src/routes/organization/assets.ts | 7 +++++- .../lesson/video/lesson-video-player.svelte | 23 +++++++++++++++---- .../src/lib/utils/translations/da.json | 4 +++- .../src/lib/utils/translations/de.json | 4 +++- .../src/lib/utils/translations/en.json | 2 ++ .../src/lib/utils/translations/es.json | 4 +++- .../src/lib/utils/translations/fr.json | 4 +++- .../src/lib/utils/translations/hi.json | 4 +++- .../src/lib/utils/translations/pl.json | 4 +++- .../src/lib/utils/translations/pt.json | 4 +++- .../src/lib/utils/translations/ru.json | 4 +++- .../src/lib/utils/translations/vi.json | 4 +++- .../media-player/players/plyr-player.svelte | 23 +++++++++++++++---- packages/ui/src/custom/media-player/types.ts | 5 +++- 15 files changed, 81 insertions(+), 21 deletions(-) diff --git a/apps/api/src/routes/org-site/public-course.ts b/apps/api/src/routes/org-site/public-course.ts index bd631317..f2a658e3 100644 --- a/apps/api/src/routes/org-site/public-course.ts +++ b/apps/api/src/routes/org-site/public-course.ts @@ -75,7 +75,11 @@ export const publicCourseRouter = new Hono() path: '/', httpOnly: true, secure: true, - sameSite: 'Lax', + // `None` (not `Lax`) so the cookie is sent on cross-site segment + // fetches issued by hls.js — Lax is withheld on cross-site + // subresource requests, which would 403 every segment when the + // public site and api/stream live on different hosts. + sameSite: 'None', maxAge: result.maxAgeSeconds }); diff --git a/apps/api/src/routes/organization/assets.ts b/apps/api/src/routes/organization/assets.ts index e856c880..72cb11e0 100644 --- a/apps/api/src/routes/organization/assets.ts +++ b/apps/api/src/routes/organization/assets.ts @@ -261,7 +261,12 @@ export const assetsRouter = new Hono() path: '/', httpOnly: true, secure: true, - sameSite: 'Lax', + // `None` (not `Lax`) so the cookie is sent on cross-site segment + // fetches issued by hls.js — Lax is withheld on cross-site + // subresource requests, which would 403 every segment when the + // player and api/stream live on different hosts (BYOD custom + // domains, local cross-origin dev). + sameSite: 'None', maxAge: maxAgeSeconds }); diff --git a/apps/dashboard/src/lib/features/course/components/lesson/video/lesson-video-player.svelte b/apps/dashboard/src/lib/features/course/components/lesson/video/lesson-video-player.svelte index a8f9aa12..4aa91fe8 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/video/lesson-video-player.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/video/lesson-video-player.svelte @@ -6,7 +6,9 @@ import { jobsApi, JobPoller, type MediaJobEnvelope } from '$features/jobs'; import { t } from '$lib/utils/functions/translations'; import { sidePanel } from '$features/side-panel'; - import { classroomio } from '$lib/utils/services/api'; + import { classroomio, isAuthError } from '$lib/utils/services/api'; + import { goto } from '$app/navigation'; + import { resolve } from '$app/paths'; import { lessonApi } from '$features/course/api'; import { snackbar } from '$features/ui/snackbar/store'; import type { AssetTranscriptPayload } from '$features/media/utils/types'; @@ -43,10 +45,18 @@ return built.toString().replace(/\/\*$/, '') + '/' + rest; } - async function mintHlsCookie(assetId: string): Promise { - await classroomio.organization.assets[':assetId'].hls.cookie.$post({ - param: { assetId } - }); + async function mintHlsCookie(assetId: string): Promise<{ authExpired?: boolean } | void> { + try { + await classroomio.organization.assets[':assetId'].hls.cookie.$post({ + param: { assetId } + }); + } catch (error) { + // A 401 here means the viewer's session expired — the player should + // prompt a re-login rather than retry. Other failures (e.g. 503 when + // HLS_SIGNING_SECRET is unset locally) fall through so playback can + // still attempt the session-auth streaming route. + if (isAuthError(error)) return { authExpired: true }; + } } /** @@ -473,6 +483,9 @@ playbackErrorLabel: $t('course.navItem.lessons.materials.tabs.video.playback_error'), playbackReloadLabel: $t('course.navItem.lessons.materials.tabs.video.playback_reload'), onPlaybackReload: handlePlaybackReload, + playbackAuthErrorLabel: $t('course.navItem.lessons.materials.tabs.video.playback_auth_error'), + playbackAuthActionLabel: $t('course.navItem.lessons.materials.tabs.video.playback_auth_action'), + onPlaybackAuthRequired: () => goto(resolve('/login', {})), seekPolicy }} /> diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index 321dad86..e5be5253 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Genererer 1080p...", "generate_1080_ready": "1080p tilgængelig", "generate_1080_unavailable": "1080p ikke tilgængelig for denne kildevideo", - "loading": "Indlæser video..." + "loading": "Indlæser video...", + "playback_auth_error": "Din session udløb. Log venligst ind igen for at se denne video.", + "playback_auth_action": "Log ind" }, "slide": { "title": "Slide", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 7e18ffc6..e40d05a9 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Generiert 1080p…", "generate_1080_ready": "1080p verfügbar", "generate_1080_unavailable": "1080p ist für dieses Quellvideo nicht verfügbar", - "loading": "Video wird geladen…" + "loading": "Video wird geladen…", + "playback_auth_error": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an, um dieses Video anzusehen.", + "playback_auth_action": "anmelden" }, "slide": { "title": "Rutsche", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index fe513bfe..0f09e873 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -2467,6 +2467,8 @@ "generate_1080_unavailable": "1080p unavailable for this source video", "playback_error": "Loading this video failed.", "playback_reload": "Reload video", + "playback_auth_error": "Your session expired. Please sign in again to watch this video.", + "playback_auth_action": "Sign in", "loading": "Loading video…", "simple_card": { "kind_youtube": "YouTube", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index cbfbf50a..76b722b9 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Generando 1080p…", "generate_1080_ready": "1080p disponible", "generate_1080_unavailable": "1080p no disponible para este vídeo fuente", - "loading": "Cargando vídeo…" + "loading": "Cargando vídeo…", + "playback_auth_error": "Tu sesión expiró. Inicie sesión nuevamente para ver este video.", + "playback_auth_action": "Iniciar sesión" }, "slide": { "title": "diapositiva", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 2cf6ff0b..912b1c27 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Génération de 1080p…", "generate_1080_ready": "1080p disponible", "generate_1080_unavailable": "1080p indisponible pour cette vidéo source", - "loading": "Chargement de la vidéo…" + "loading": "Chargement de la vidéo…", + "playback_auth_error": "Votre session a expiré. Veuillez vous reconnecter pour regarder cette vidéo.", + "playback_auth_action": "Se connecter" }, "slide": { "title": "Diapositive", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 76c6c5d0..16d1efd4 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "1080p जेनरेट हो रहा है...", "generate_1080_ready": "1080p उपलब्ध", "generate_1080_unavailable": "इस स्रोत वीडियो के लिए 1080p अनुपलब्ध है", - "loading": "वीडियो लोड हो रहा है..." + "loading": "वीडियो लोड हो रहा है...", + "playback_auth_error": "आपका सत्र समाप्त हो गया. कृपया इस वीडियो को देखने के लिए फिर से साइन इन करें।", + "playback_auth_action": "दाखिल करना" }, "slide": { "title": "फिसल पट्टी", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index b6b1c465..409d286c 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Generowanie 1080p…", "generate_1080_ready": "Dostępna rozdzielczość 1080p", "generate_1080_unavailable": "Rozdzielczość 1080p jest niedostępna dla tego wideo źródłowego", - "loading": "Ładowanie wideo…" + "loading": "Ładowanie wideo…", + "playback_auth_error": "Twoja sesja wygasła. Aby obejrzeć ten film, zaloguj się ponownie.", + "playback_auth_action": "Zalogować się" }, "slide": { "title": "Przesuń", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index acb9b107..4e072472 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Gerando 1080p…", "generate_1080_ready": "1080p disponível", "generate_1080_unavailable": "1080p indisponível para este vídeo de origem", - "loading": "Carregando vídeo…" + "loading": "Carregando vídeo…", + "playback_auth_error": "Sua sessão expirou. Faça login novamente para assistir a este vídeo.", + "playback_auth_action": "Entrar" }, "slide": { "title": "Deslizar", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 1d652ea5..2b6f882c 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Генерация 1080p…", "generate_1080_ready": "доступно 1080p", "generate_1080_unavailable": "Разрешение 1080p недоступно для этого исходного видео.", - "loading": "Загрузка видео…" + "loading": "Загрузка видео…", + "playback_auth_error": "Срок действия вашей сессии истек. Пожалуйста, войдите еще раз, чтобы посмотреть это видео.", + "playback_auth_action": "Войти" }, "slide": { "title": "Слайд", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index c644aa15..bf7af053 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -1998,7 +1998,9 @@ "generate_1080_in_progress": "Đang tạo 1080p…", "generate_1080_ready": "1080p có sẵn", "generate_1080_unavailable": "1080p không có sẵn cho video nguồn này", - "loading": "Đang tải video…" + "loading": "Đang tải video…", + "playback_auth_error": "Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại để xem video này.", + "playback_auth_action": "Đăng nhập" }, "slide": { "title": "Trượt", 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 96e16704..d82e103f 100644 --- a/packages/ui/src/custom/media-player/players/plyr-player.svelte +++ b/packages/ui/src/custom/media-player/players/plyr-player.svelte @@ -32,6 +32,7 @@ let playerInstance = $state(null); let transcriptButtonElement = $state(null); let playbackFailed = $state(false); + let playbackAuthExpired = $state(false); let isPlayerReady = $state(false); let reloadInFlight = $state(false); let timeupdateCleanup: (() => void) | undefined; @@ -533,6 +534,7 @@ loadedSrc = nextSrc; playbackFailed = false; + playbackAuthExpired = false; const onLoadedMetadata = () => { element.removeEventListener('loadedmetadata', onLoadedMetadata); @@ -566,10 +568,17 @@ destroyHlsInstance(); teardownPlyrInstance(); playbackFailed = false; + playbackAuthExpired = false; if (options.onBeforeHlsLoad) { try { - await options.onBeforeHlsLoad(); + const result = await options.onBeforeHlsLoad(); + if (result?.authExpired) { + playbackAuthExpired = true; + handlePlaybackFailure(); + + return; + } } catch (error) { // The cookie endpoint may 503 in environments where HLS_SIGNING_SECRET // isn't configured (local dev / self-hosted). Playback can still @@ -750,13 +759,19 @@ {/if} - {#if playbackFailed && options.playbackErrorLabel} + {#if playbackFailed && (playbackAuthExpired ? options.playbackAuthErrorLabel : options.playbackErrorLabel)}