SharedObservatoryPage.tsx 6.53 KB
import { useState, useEffect } from "react";
import { Video } from "lucide-react";
import {
  KIOSK_EVENT,
  loadVideoSourceConfig,
  type VideoSourceConfig,
} from "../kioskStorage";
import { PAGE_CONTENT_INSET } from "../pageContentInset";

export function SharedObservatoryPage() {
  const [videoSource, setVideoSource] = useState<VideoSourceConfig>(() => loadVideoSourceConfig());
  const [hlsPlaybackError, setHlsPlaybackError] = useState(false);

  useEffect(() => {
    const sync = () => setVideoSource(loadVideoSourceConfig());
    window.addEventListener(KIOSK_EVENT, sync);
    window.addEventListener("storage", sync);
    return () => {
      window.removeEventListener(KIOSK_EVENT, sync);
      window.removeEventListener("storage", sync);
    };
  }, []);

  useEffect(() => {
    setHlsPlaybackError(false);
  }, [videoSource.protocol, videoSource.url]);

  const streamUrl = videoSource.url.trim();
  const hasStream = streamUrl !== "";
  const showHlsPlayer = hasStream && videoSource.protocol === "HLS" && !hlsPlaybackError;
  const canEmbedByIframe =
    hasStream &&
    (streamUrl.startsWith("http://") || streamUrl.startsWith("https://")) &&
    (!showHlsPlayer || hlsPlaybackError);

  return (
    <div
      className={`flex min-h-0 min-w-0 w-full flex-1 flex-col overflow-hidden ${PAGE_CONTENT_INSET}`}
    >
      <div className="flex min-h-0 w-full min-w-0 flex-1 flex-col">
        <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
          {/* 实时视频对接区:占满主区域剩余高度,随屏伸缩 */}
          <section className="flex min-h-0 min-w-0 flex-1 flex-col" aria-labelledby="live-video-heading">
            <h2
              id="live-video-heading"
              className="mb-3 flex shrink-0 items-center gap-2 text-xl font-bold text-white sm:text-2xl"
            >
              <Video className="h-6 w-6 text-sky-400" />
              望远镜实时视频
            </h2>
            <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-white/20 bg-black/40 shadow-xl shadow-black/30">
              <div className="relative min-h-0 w-full flex-1">
                {showHlsPlayer ? (
                  <video
                    src={streamUrl}
                    className="absolute inset-0 z-[1] h-full w-full object-cover"
                    controls
                    autoPlay
                    muted
                    playsInline
                    onError={() => setHlsPlaybackError(true)}
                  />
                ) : null}
                {canEmbedByIframe ? (
                  <iframe
                    src={streamUrl}
                    title="望远镜视频源"
                    className="absolute inset-0 z-[1] h-full w-full border-0 bg-black"
                    allow="autoplay; fullscreen; picture-in-picture; camera; microphone"
                    allowFullScreen
                  />
                ) : null}
                <div
                  className={`absolute inset-0 ${showHlsPlayer ? "z-[2]" : ""} bg-gradient-to-br from-slate-900 via-blue-950 to-slate-950`}
                  aria-hidden
                />
                <div
                  className={`absolute inset-0 ${showHlsPlayer ? "z-[2]" : ""} opacity-40`}
                  style={{
                    backgroundImage:
                      "radial-gradient(circle at 20% 30%, rgba(125,211,252,0.18) 0%, transparent 45%), radial-gradient(circle at 80% 70%, rgba(59,130,246,0.22) 0%, transparent 50%)",
                  }}
                  aria-hidden
                />
                <div className="relative flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
                  <Video className="h-14 w-14 text-white/35 sm:h-16 sm:w-16" aria-hidden />
                  <p className="text-lg font-medium text-white/90 sm:text-xl">
                    {hasStream ? "已配置视频源" : "等待接入望远镜视频信号"}
                  </p>
                  {hasStream ? (
                    <div className="w-full max-w-3xl space-y-2">
                      {videoSource.protocol !== "HLS" || hlsPlaybackError ? (
                        <p className="w-full max-w-full px-2 text-sm text-blue-200/90">
                          当前协议为 {videoSource.protocol}
                          {videoSource.protocol === "RTSP"
                            ? ",浏览器通常不能直接播放 RTSP,请通过 WebRTC/HLS 网关转封装。"
                            : ",当前页面展示为已接入状态信息。"}
                        </p>
                      ) : (
                        <p className="w-full max-w-full px-2 text-sm text-blue-200/90">
                          已尝试播放 HLS 源;如画面异常,请检查源地址、跨域与鉴权配置。
                        </p>
                      )}
                      <div className="mx-auto max-w-full break-all rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-left text-xs text-blue-100">
                        {streamUrl}
                      </div>
                      {videoSource.note ? (
                        <div className="mx-auto max-w-full rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-left text-xs text-blue-200/90">
                          备注:{videoSource.note}
                        </div>
                      ) : null}
                      {canEmbedByIframe ? (
                        <p className="text-xs text-blue-200/85">
                          已在当前页面内嵌加载视频源页面。
                        </p>
                      ) : null}
                    </div>
                  ) : (
                    <p className="w-full max-w-full px-2 text-sm text-blue-200/90">
                      未检测到推流。配置信号源地址与鉴权后,画面将在此区域实时显示。
                    </p>
                  )}
                  <div className="mt-2 flex flex-wrap items-center justify-center gap-2">
                    <span className="rounded-full border border-sky-400/40 bg-sky-500/15 px-3 py-1 text-xs font-medium text-sky-200">
                      状态:{hasStream ? "已配置" : "未连接"}
                    </span>
                    <span className="rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs text-blue-200">
                      对接方式:{videoSource.protocol}
                    </span>
                  </div>
                </div>
              </div>
            </div>
          </section>
        </div>
      </div>
    </div>
  );
}