fix: hls session ending and user can't watch video
This commit is contained in:
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
+15
-2
@@ -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<void> {
|
||||
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
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "फिसल पट्टी",
|
||||
|
||||
@@ -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ń",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Слайд",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
let playerInstance = $state<Plyr | null>(null);
|
||||
let transcriptButtonElement = $state<HTMLButtonElement | null>(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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if playbackFailed && options.playbackErrorLabel}
|
||||
{#if playbackFailed && (playbackAuthExpired ? options.playbackAuthErrorLabel : options.playbackErrorLabel)}
|
||||
<div
|
||||
class="ui:absolute ui:inset-0 ui:z-20 ui:flex ui:flex-col ui:items-center ui:justify-center ui:gap-4 ui:bg-muted/95 ui:px-6 ui:text-center"
|
||||
role="alert"
|
||||
>
|
||||
<p class="ui:text-sm ui:text-muted-foreground">{options.playbackErrorLabel}</p>
|
||||
{#if options.onPlaybackReload && options.playbackReloadLabel}
|
||||
<p class="ui:text-sm ui:text-muted-foreground">
|
||||
{playbackAuthExpired ? options.playbackAuthErrorLabel : options.playbackErrorLabel}
|
||||
</p>
|
||||
{#if playbackAuthExpired && options.onPlaybackAuthRequired && options.playbackAuthActionLabel}
|
||||
<Button variant="outline" onclick={() => options.onPlaybackAuthRequired?.()}>
|
||||
{options.playbackAuthActionLabel}
|
||||
</Button>
|
||||
{:else if !playbackAuthExpired && options.onPlaybackReload && options.playbackReloadLabel}
|
||||
<Button variant="outline" disabled={reloadInFlight} onclick={() => void handlePlaybackReloadClick()}>
|
||||
{options.playbackReloadLabel}
|
||||
</Button>
|
||||
|
||||
@@ -50,6 +50,9 @@ export interface MediaPlayerOptions {
|
||||
playbackReloadLabel?: string;
|
||||
/** Fetch a fresh playback URL (or otherwise prepare a retry). Return true to retry loading. */
|
||||
onPlaybackReload?: () => boolean | Promise<boolean>;
|
||||
playbackAuthErrorLabel?: string;
|
||||
playbackAuthActionLabel?: string;
|
||||
onPlaybackAuthRequired?: () => void;
|
||||
/** Fires after a new `src` finishes loading metadata (used to restore seek position). */
|
||||
onSourceLoaded?: (video: HTMLVideoElement) => void;
|
||||
/**
|
||||
@@ -67,7 +70,7 @@ export interface MediaPlayerOptions {
|
||||
* production. Locally / self-hosted this is typically a no-op or a
|
||||
* best-effort call that fails benignly.
|
||||
*/
|
||||
onBeforeHlsLoad?: () => Promise<void>;
|
||||
onBeforeHlsLoad?: () => Promise<{ authExpired?: boolean } | void>;
|
||||
seekPolicy?: {
|
||||
mode: 'locked_until_complete';
|
||||
watchThresholdPercent: number;
|
||||
|
||||
Reference in New Issue
Block a user