Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Camera Feed Fixes: Gracefully handle full-screen errors for unsupported devices; fixes clipping of content in certain sizes in landscape mode #7965

Merged
merged 8 commits into from
Jun 3, 2024
15 changes: 12 additions & 3 deletions src/CAREUI/misc/Fullscreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ interface Props {
fullscreenClassName?: string;
children: React.ReactNode;
fullscreen: boolean;
onExit: () => void;
onExit: (reason?: "DEVICE_UNSUPPORTED") => void;
}

export default function Fullscreen(props: Props) {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!ref.current) {
return;
}

if (props.fullscreen) {
ref.current?.requestFullscreen();
if (ref.current.requestFullscreen) {
ref.current.requestFullscreen();
} else {
props.onExit("DEVICE_UNSUPPORTED");
}
} else {
document.exitFullscreen();
document.exitFullscreen?.();
}
}, [props.fullscreen]);

Expand All @@ -27,6 +35,7 @@ export default function Fullscreen(props: Props) {
};

document.addEventListener("fullscreenchange", listener);

return () => {
document.removeEventListener("fullscreenchange", listener);
};
Expand Down
19 changes: 15 additions & 4 deletions src/Components/CameraFeed/AssetBedSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,35 @@ export default function CameraPresetSelect(props: Props) {
{props.options.length > 5 && (
<CameraPresetDropdown
{...props}
placeholder="More preset"
options={props.options.slice(4)}
value={props.options.slice(4).find((o) => o.id === props.value?.id)}
/>
)}
</div>
<div className="w-full lg:hidden">
{/* Mobile View */}
<CameraPresetDropdown {...props} />
<CameraPresetDropdown {...props} placeholder="Select preset" />
</div>
</>
);
}

export const CameraPresetDropdown = (props: Props) => {
export const CameraPresetDropdown = (
props: Props & { placeholder: string },
) => {
const selected = props.value;

const options = props.options.filter(({ meta }) => meta.type !== "boundary");

const label = props.label ?? defaultLabel;

return (
<Listbox value={selected} onChange={props.onChange}>
<Listbox
value={selected}
onChange={props.onChange}
disabled={options.length === 0}
>
<div className="relative flex-1">
<Listbox.Button
className={classNames(
Expand All @@ -67,7 +74,11 @@ export const CameraPresetDropdown = (props: Props) => {
)}
>
<span className="block truncate">
{selected ? label(selected) : "Select preset"}
{options.length === 0
? "No presets"
: selected
? label(selected)
: props.placeholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 mr-1 mt-1 flex items-center">
<CareIcon icon="l-angle-down" className="text-xl text-zinc-400" />
Expand Down
41 changes: 30 additions & 11 deletions src/Components/CameraFeed/CameraFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import useOperateCamera, { PTZPayload } from "./useOperateCamera";
import usePlayer from "./usePlayer";
import { getStreamUrl } from "./utils";
import ReactPlayer from "react-player";
import { classNames, isIOS } from "../../Utils/utils";
import { classNames, isAppleDevice, isIOS } from "../../Utils/utils";
import FeedAlert, { FeedAlertState } from "./FeedAlert";
import FeedNetworkSignal from "./FeedNetworkSignal";
import NoFeedAvailable from "./NoFeedAvailable";
import FeedControls from "./FeedControls";
import Fullscreen from "../../CAREUI/misc/Fullscreen";
import FeedWatermark from "./FeedWatermark";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { Error } from "../../Utils/Notifications";

interface Props {
children?: React.ReactNode;
Expand All @@ -27,6 +28,7 @@ interface Props {
constrolsDisabled?: boolean;
shortcutsDisabled?: boolean;
onMove?: () => void;
onReset?: () => void;
}

export default function CameraFeed(props: Props) {
Expand Down Expand Up @@ -86,14 +88,29 @@ export default function CameraFeed(props: Props) {

const resetStream = () => {
setState("loading");
props.onReset?.();
initializeStream();
};
return (
<Fullscreen fullscreen={isFullscreen} onExit={() => setFullscreen(false)}>
<Fullscreen
fullscreen={isFullscreen}
onExit={(reason) => {
setFullscreen(false);

if (reason === "DEVICE_UNSUPPORTED") {
// iOS webkit allows only video/iframe elements to call full-screen
// APIs. But we need to show controls too, not just the video element.
Error({
msg: "This device does not support viewing this content in full-screen.",
});
}
}}
>
<div
className={classNames(
"flex max-h-screen flex-col overflow-clip rounded-xl bg-black",
"flex flex-col overflow-clip rounded-xl bg-black md:max-h-screen",
props.className,
isAppleDevice && isFullscreen && "px-20",
)}
>
<div className="flex items-center justify-between bg-zinc-900 px-4 py-1.5 md:py-2">
Expand All @@ -106,14 +123,16 @@ export default function CameraFeed(props: Props) {
/>
{props.asset.name}
</span>
<div className={state === "loading" ? "animate-pulse" : ""}>
<FeedNetworkSignal
playerRef={playerRef as any}
playedOn={player.playedOn}
status={player.status}
onReset={resetStream}
/>
</div>
{!isIOS && (
<div className={state === "loading" ? "animate-pulse" : ""}>
<FeedNetworkSignal
playerRef={playerRef as any}
playedOn={player.playedOn}
status={player.status}
onReset={resetStream}
/>
</div>
)}
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions src/Components/CameraFeed/CameraFeedWithBedPresets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function LocationFeedTile(props: Props) {
options={data?.results ?? []}
value={preset}
onChange={setPreset}
placeholder="Select preset"
/>
)}
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/Components/CameraFeed/FeedAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ interface Props {
state?: FeedAlertState;
}

const ALERT_ICON_MAP: Record<FeedAlertState, IconName> = {
const ALERT_ICON_MAP: Partial<Record<FeedAlertState, IconName>> = {
playing: "l-play-circle",
stop: "l-stop-circle",
offline: "l-exclamation-triangle",
loading: "l-spinner",
moving: "l-expand-from-corner",
// moving: "l-expand-from-corner",
zooming: "l-search",
saving_preset: "l-save",
host_unreachable: "l-exclamation-triangle",
Expand Down Expand Up @@ -53,14 +53,14 @@ export default function FeedAlert({ state }: Props) {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 -translate-y-5"
>
<div className="absolute right-8 top-6 flex items-center gap-1.5 rounded bg-white/20 px-2 py-1 text-white">
{state && (
<div className="absolute right-8 top-4 z-20 flex items-center gap-1.5 rounded bg-white/20 px-2 py-1 text-white">
{state && ALERT_ICON_MAP[state] && (
<CareIcon
className={classNames(
"text-base",
state === "loading" && "animate-spin",
)}
icon={ALERT_ICON_MAP[state]}
icon={ALERT_ICON_MAP[state]!}
/>
)}
<span className="text-xs font-medium capitalize">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import useOperateCamera, {
PTZPayload,
} from "../../CameraFeed/useOperateCamera";
import request from "../../../Utils/request/request";
import { classNames } from "../../../Utils/utils";
import { classNames, isIOS } from "../../../Utils/utils";

export const ConsultationFeedTab = (props: ConsultationTabProps) => {
const authUser = useAuthUser();
Expand All @@ -27,6 +27,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
const [preset, setPreset] = useState<AssetBedModel>();
const [isUpdatingPreset, setIsUpdatingPreset] = useState(false);
const [hasMoved, setHasMoved] = useState(false);
const [key, setKey] = useState(0);
const divRef = useRef<any>();

const operate = useOperateCamera(asset?.id ?? "", true);
Expand Down Expand Up @@ -106,9 +107,15 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
</span>
<div ref={divRef}>
<CameraFeed
key={key}
asset={asset}
preset={preset?.meta.position}
onMove={() => setHasMoved(true)}
onReset={() => {
if (isIOS) {
setKey(key + 1);
}
}}
onStreamError={() => {
triggerGoal("Camera Feed Viewed", {
consultationId: props.consultationId,
Expand Down
Loading