"use client";
import { useEffect, useRef } from "react";
import {
MediaController,
MediaControlBar,
MediaVolumeRange,
MediaPlayButton,
MediaMuteButton,
MediaFullscreenButton,
} from "media-chrome/react";
import { ENV } from "@/lib/haAuth";
import { useWebRTC, type WebRTCConnection } from "@/lib/video-rtc";
import { StatusIndicator } from "@/components/ha-ui/ui/status-indicator";
import { RetryButton } from "@/components/ha-ui/ui/retry-button";
import type { EntityId } from "@/types/entity-types";
import { cn } from "@/lib/utils";
type AspectRatio = "1/1" | "4/3" | "16/9";
export interface CameraFeedProps {
/**
* HomeAssistant Entity Name, used to generate Websocket URL for go2rtc stream
*/
entity: EntityId;
/**
* Optional: Explicit WebSocket override URL (e.g. ws://localhost:5555/websocketVideo)
*/
wsURL?: string;
/**
* Optional: Proxy server to forward websocket requests over (e.g. ws://localhost:5555/proxy)
* (Default: None)
*/
proxyURL?: string;
/**
* Optional: Disable video controls
* (Default: False)
*/
disableControls?: boolean;
/**
* Optional: Aspect Ratio for component to follow,
* Will Letterbox to meet ratio
* (Default: 16/9)
* */
aspectRatio?: AspectRatio;
/**
* Optional: Camera Source override
* Will ignore entity, wsURL and proxyURl args.
*/
externalCameraSource?: WebRTCConnection;
}
export function Camera({
entity,
wsURL,
proxyURL,
externalCameraSource,
disableControls = false,
aspectRatio = "16/9",
}: CameraFeedProps) {
const webRTC = useWebRTC({
wsSrc: wsURL || `ws://${ENV.HA_HOST}:11984/api/ws?src=${entity}`,
proxy: proxyURL,
media: "video,audio",
enabled: !externalCameraSource,
autoReconnect: true,
retryDelay: 5000,
maxReconnectAttempts: 5,
});
// Use external source if provided, otherwise use hook's stream
const mediaStream = externalCameraSource?.mediaStream || webRTC.mediaStream;
const status = externalCameraSource ? externalCameraSource.status : webRTC.status;
const error = externalCameraSource ? externalCameraSource.error : webRTC.error;
const videoRef = useRef<HTMLVideoElement>(null);
// Attach stream to video element
useEffect(() => {
if (mediaStream && videoRef.current) {
videoRef.current.srcObject = mediaStream;
} else if (!mediaStream && videoRef.current) {
videoRef.current.srcObject = null;
}
}, [mediaStream]);
const aspectRatioMap: Record<AspectRatio, string> = {
"1/1": "aspect-square",
"16/9": "aspect-video",
"4/3": "aspect-[4/3]",
};
return (
<MediaController className={cn("group w-full", aspectRatioMap[aspectRatio])}>
{/* Video Element */}
<video
slot="media"
ref={videoRef}
autoPlay
muted
playsInline
controls={false}
className={aspectRatioMap[aspectRatio]}
/>
{/* Status Indicator (top right) */}
<StatusIndicator status={status} error={error} />
{/* Retry Button (centered overlay) */}
{externalCameraSource ? (
<RetryButton onRetry={externalCameraSource.retry} status={status} error={error} autoReconnect={true} />
) : (
<RetryButton onRetry={webRTC.retry} status={status} error={error} autoReconnect={true} />
)}
{/* Video Controls Overlay */}
{!disableControls && (
<MediaControlBar className="relative flex w-full justify-between bg-black px-4 transition-opacity duration-200 md:absolute md:right-0 md:bottom-0 md:left-0 md:z-10 md:opacity-0 md:group-hover:opacity-100">
<MediaPlayButton className="bg-black px-2 hover:bg-slate-800" />
<div className="text-white">
<MediaMuteButton className="bg-black p-2 hover:bg-slate-800"></MediaMuteButton>
<MediaVolumeRange className="bg-black px-2 hover:bg-slate-800"></MediaVolumeRange>
<MediaFullscreenButton className="h-full bg-black px-2 hover:bg-slate-800"></MediaFullscreenButton>
</div>
</MediaControlBar>
)}
</MediaController>
);
}