162 lines
5.2 KiB
Svelte
162 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import { goto } from "$app/navigation";
|
|
import {
|
|
checkLinkValidityFresh,
|
|
mapLinkErrorToSessionEndReason,
|
|
} from "$lib/domains/link/client";
|
|
import { endLinkSessionSC } from "$lib/domains/link/link.remote";
|
|
import { onMount } from "svelte";
|
|
|
|
const POLL_INTERVAL_MS = 2000;
|
|
const MAX_CONSECUTIVE_POLL_ERRORS = 3;
|
|
|
|
let state: "loading" | "ready" = "loading";
|
|
let token = "";
|
|
let streamUrl: string | null = null;
|
|
let statusText = "Connecting to your active session...";
|
|
|
|
onMount(() => {
|
|
let disposed = false;
|
|
let hasEnded = false;
|
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
let inFlight = false;
|
|
let consecutivePollErrors = 0;
|
|
|
|
const endSession = async (reason: string) => {
|
|
if (disposed || hasEnded) return;
|
|
hasEnded = true;
|
|
|
|
if (timer) clearInterval(timer);
|
|
streamUrl = null;
|
|
|
|
if (token) {
|
|
try {
|
|
await endLinkSessionSC({ token });
|
|
} catch {
|
|
// Best-effort cleanup; user ejection should proceed regardless.
|
|
}
|
|
}
|
|
|
|
await goto(`/session-ended?reason=${encodeURIComponent(reason)}`, {
|
|
replaceState: true,
|
|
});
|
|
};
|
|
|
|
const pollValidity = async () => {
|
|
if (disposed || inFlight || !token) return;
|
|
inFlight = true;
|
|
|
|
try {
|
|
const result = await checkLinkValidityFresh(token);
|
|
|
|
if (disposed) return;
|
|
|
|
if (result?.error || !result?.data) {
|
|
await endSession(
|
|
mapLinkErrorToSessionEndReason(result?.error),
|
|
);
|
|
return;
|
|
}
|
|
|
|
consecutivePollErrors = 0;
|
|
|
|
if (result.data.link.status !== "active") {
|
|
await endSession("expired");
|
|
return;
|
|
}
|
|
|
|
const nextStreamUrl = result.data.device?.streamUrl;
|
|
if (typeof nextStreamUrl === "string" && nextStreamUrl.length > 0) {
|
|
streamUrl = nextStreamUrl;
|
|
}
|
|
} catch {
|
|
consecutivePollErrors += 1;
|
|
if (consecutivePollErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {
|
|
await endSession("network");
|
|
}
|
|
} finally {
|
|
inFlight = false;
|
|
}
|
|
};
|
|
|
|
const start = async () => {
|
|
token =
|
|
new URLSearchParams(window.location.search)
|
|
.get("token")
|
|
?.trim() || "";
|
|
|
|
if (!token) {
|
|
await goto("/unauthorized?reason=missing_token", {
|
|
replaceState: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const initial = await checkLinkValidityFresh(token);
|
|
if (initial?.error || !initial?.data) {
|
|
await endSession(mapLinkErrorToSessionEndReason(initial?.error));
|
|
return;
|
|
}
|
|
|
|
streamUrl = initial.data.device?.streamUrl ?? null;
|
|
state = "ready";
|
|
statusText = "Session active";
|
|
|
|
await pollValidity();
|
|
timer = setInterval(() => {
|
|
void pollValidity();
|
|
}, POLL_INTERVAL_MS);
|
|
};
|
|
|
|
void start();
|
|
|
|
return () => {
|
|
disposed = true;
|
|
if (timer) clearInterval(timer);
|
|
if (!hasEnded && token) {
|
|
void endLinkSessionSC({ token });
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<main class="bg-background text-foreground min-h-dvh">
|
|
{#if state === "loading"}
|
|
<section class="flex min-h-dvh items-center justify-center px-6">
|
|
<div
|
|
class="w-full max-w-sm rounded-xl border border-border bg-card p-6 text-center"
|
|
>
|
|
<h1 class="text-xl font-semibold">Loading session</h1>
|
|
<p class="mt-3 text-sm text-muted-foreground">
|
|
{statusText}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
{:else}
|
|
<section class="bg-black relative h-dvh min-h-dvh overflow-hidden">
|
|
<div
|
|
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center px-3 pt-2"
|
|
>
|
|
<div class="rounded-full bg-black/55 px-3 py-1 text-xs text-white/90">
|
|
{statusText}
|
|
</div>
|
|
</div>
|
|
<div class="h-full w-full">
|
|
{#if streamUrl}
|
|
<iframe
|
|
src={streamUrl}
|
|
title="Android session"
|
|
class="block h-full w-full border-0 bg-black"
|
|
allow="autoplay; fullscreen"
|
|
referrerpolicy="no-referrer"
|
|
></iframe>
|
|
{:else}
|
|
<div class="flex h-full items-center justify-center">
|
|
<p class="text-sm text-white/70">Waiting for stream...</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
</main>
|