import { appBasePath } from "@agent-native/core/client/api-path"; import { useT } from "@agent-native/core/client/i18n"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useSearchParams } from "react-router"; import { AccessPasswordPrompt } from "@/components/player/access-password-prompt"; import { VideoPlayer, type VideoPlayerHandle, } from "@/components/player/video-player"; import { Spinner } from "@/components/ui/spinner"; import { useViewTracking } from "@/hooks/use-view-tracking"; import { parsePlaybackSpeed } from "@/lib/playback-speed"; import { isLoomEmbedBackedRecording } from "../../shared/loom"; export function meta() { return [{ title: "Clip" }]; } const STORAGE_KEY_PREFIX = "clips-share-pw-"; /** * Parse `t` URL param into ms (supports plain seconds or `1m20s` / `1h2m3s`). * "80" → 80_000 * "1m20s" → 80_000 * "1h2m3s" → 3_723_000 * "1:20" → 80_000 (MM:SS) */ function parseTimeParam(raw: string | null): number { if (!raw) return 0; const v = raw.trim(); if (!v) return 0; // Plain seconds if (/^\d+(\.\d+)?$/.test(v)) return Math.floor(parseFloat(v) * 1000); // MM:SS or HH:MM:SS if (/^\d+:\d+(:\d+)?$/.test(v)) { const parts = v.split(":").map((n) => parseInt(n, 10)); if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000; if (parts.length === 3) return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000; } // 1h2m3s style const m = v.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i); if (m) { const h = parseInt(m[1] ?? "0", 10); const mm = parseInt(m[2] ?? "0", 10); const s = parseInt(m[3] ?? "0", 10); return (h * 3600 + mm * 60 + s) * 1000; } return 0; } export default function EmbedRoute() { const t = useT(); const { shareId } = useParams<{ shareId: string }>(); const [searchParams] = useSearchParams(); const playerRef = useRef(null); const autoplay = searchParams.get("autoplay") === "1"; const hideControls = searchParams.get("hideControls") === "1"; const hideCaptions = searchParams.get("hideCaptions") === "1"; const startMs = useMemo( () => parseTimeParam(searchParams.get("t")), [searchParams], ); const [password, setPassword] = useState(() => { if (typeof window === "undefined" || !shareId) return null; try { return sessionStorage.getItem(STORAGE_KEY_PREFIX + shareId); } catch { return null; } }); const [pwError, setPwError] = useState(null); useEffect(() => { if (typeof document === "undefined") return; const html = document.documentElement; const body = document.body; const previous = { htmlOverflow: html.style.overflow, htmlHeight: html.style.height, bodyOverflow: body.style.overflow, bodyHeight: body.style.height, bodyBackground: body.style.background, }; html.style.overflow = "hidden"; html.style.height = "100%"; body.style.overflow = "hidden"; body.style.height = "100%"; body.style.background = "#000"; return () => { html.style.overflow = previous.htmlOverflow; html.style.height = previous.htmlHeight; body.style.overflow = previous.bodyOverflow; body.style.height = previous.bodyHeight; body.style.background = previous.bodyBackground; }; }, []); const dataQ = useQuery({ queryKey: ["public-recording", shareId, password], queryFn: async () => { const url = new URL( `${appBasePath()}/api/public-recording`, window.location.origin, ); url.searchParams.set("id", shareId ?? ""); if (password) url.searchParams.set("password", password); const res = await fetch(url.toString()); const data = await res.json().catch(() => ({})); return { ok: res.ok, status: res.status, data }; }, enabled: !!shareId, }); const recording = dataQ.data?.data?.recording; const comments = dataQ.data?.data?.comments ?? []; const transcriptSegments = dataQ.data?.data?.transcript?.segments ?? []; const chapters = dataQ.data?.data?.chapters ?? []; const ctas = dataQ.data?.data?.ctas ?? []; const firstCta = ctas[0] ?? null; const isLoomEmbedBacked = isLoomEmbedBackedRecording(recording); const [trackedVideoEl, setTrackedVideoEl] = useState( null, ); const tracking = useViewTracking({ recordingId: shareId ?? "", videoEl: trackedVideoEl, durationMs: recording?.durationMs ?? 0, trackOpenWithoutVideo: isLoomEmbedBacked, }); const needsPassword = dataQ.data?.status === 401 && dataQ.data.data?.passwordRequired; function onSubmitPassword(pw: string) { setPwError(null); setPassword(pw); try { sessionStorage.setItem(STORAGE_KEY_PREFIX + (shareId ?? ""), pw); } catch {} } useEffect(() => { if (needsPassword && password) { setPwError("Incorrect password"); setPassword(null); try { sessionStorage.removeItem(STORAGE_KEY_PREFIX + (shareId ?? "")); } catch {} } }, [needsPassword, password, shareId]); if (dataQ.isLoading) { return (
); } if (needsPassword) { return ( ); } if (!recording) { return (

{t("embedRoute.unavailable")}

); } return (
tracking.reportCtaClick()} alwaysShowControls={false} className="h-full w-full rounded-none" />
); }