Support opening pip from live view

This commit is contained in:
Nicolas Mowen 2024-04-01 14:32:02 -06:00
parent 4b6a3b22ed
commit eeef39c1c0
4 changed files with 65 additions and 5 deletions

View File

@ -22,6 +22,7 @@ type LivePlayerProps = {
playAudio?: boolean; playAudio?: boolean;
micEnabled?: boolean; // only webrtc supports mic micEnabled?: boolean; // only webrtc supports mic
iOSCompatFullScreen?: boolean; iOSCompatFullScreen?: boolean;
pip?: boolean;
onClick?: () => void; onClick?: () => void;
}; };
@ -35,6 +36,7 @@ export default function LivePlayer({
playAudio = false, playAudio = false,
micEnabled = false, micEnabled = false,
iOSCompatFullScreen = false, iOSCompatFullScreen = false,
pip,
onClick, onClick,
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
@ -105,6 +107,7 @@ export default function LivePlayer({
microphoneEnabled={micEnabled} microphoneEnabled={micEnabled}
iOSCompatFullScreen={iOSCompatFullScreen} iOSCompatFullScreen={iOSCompatFullScreen}
onPlaying={() => setLiveReady(true)} onPlaying={() => setLiveReady(true)}
pip={pip}
/> />
); );
} else if (liveMode == "mse") { } else if (liveMode == "mse") {
@ -116,6 +119,7 @@ export default function LivePlayer({
playbackEnabled={cameraActive} playbackEnabled={cameraActive}
audioEnabled={playAudio} audioEnabled={playAudio}
onPlaying={() => setLiveReady(true)} onPlaying={() => setLiveReady(true)}
pip={pip}
/> />
); );
} else { } else {

View File

@ -6,6 +6,7 @@ type MSEPlayerProps = {
className?: string; className?: string;
playbackEnabled?: boolean; playbackEnabled?: boolean;
audioEnabled?: boolean; audioEnabled?: boolean;
pip?: boolean;
onPlaying?: () => void; onPlaying?: () => void;
}; };
@ -14,6 +15,7 @@ function MSEPlayer({
className, className,
playbackEnabled = true, playbackEnabled = true,
audioEnabled = false, audioEnabled = false,
pip = false,
onPlaying, onPlaying,
}: MSEPlayerProps) { }: MSEPlayerProps) {
let connectTS: number = 0; let connectTS: number = 0;
@ -268,6 +270,16 @@ function MSEPlayer({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [playbackEnabled, onDisconnect, onConnect]); }, [playbackEnabled, onDisconnect, onConnect]);
// control pip
useEffect(() => {
if (!videoRef.current || !pip) {
return;
}
videoRef.current.requestPictureInPicture();
}, [pip, videoRef]);
return ( return (
<video <video
ref={videoRef} ref={videoRef}

View File

@ -8,6 +8,7 @@ type WebRtcPlayerProps = {
audioEnabled?: boolean; audioEnabled?: boolean;
microphoneEnabled?: boolean; microphoneEnabled?: boolean;
iOSCompatFullScreen?: boolean; // ios doesn't support fullscreen divs so we must support the video element iOSCompatFullScreen?: boolean; // ios doesn't support fullscreen divs so we must support the video element
pip?: boolean;
onPlaying?: () => void; onPlaying?: () => void;
}; };
@ -18,6 +19,7 @@ export default function WebRtcPlayer({
audioEnabled = false, audioEnabled = false,
microphoneEnabled = false, microphoneEnabled = false,
iOSCompatFullScreen = false, iOSCompatFullScreen = false,
pip = false,
onPlaying, onPlaying,
}: WebRtcPlayerProps) { }: WebRtcPlayerProps) {
// metadata // metadata
@ -173,8 +175,19 @@ export default function WebRtcPlayer({
]); ]);
// ios compat // ios compat
const [iOSCompatControls, setiOSCompatControls] = useState(false); const [iOSCompatControls, setiOSCompatControls] = useState(false);
// control pip
useEffect(() => {
if (!videoRef.current || !pip) {
return;
}
videoRef.current.requestPictureInPicture();
}, [pip, videoRef]);
return ( return (
<video <video
ref={videoRef} ref={videoRef}

View File

@ -51,7 +51,14 @@ import {
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi"; import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
import { HiViewfinderCircle } from "react-icons/hi2"; import { HiViewfinderCircle } from "react-icons/hi2";
import { IoMdArrowBack } from "react-icons/io"; import { IoMdArrowBack } from "react-icons/io";
import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu"; import {
LuEar,
LuEarOff,
LuPictureInPicture,
LuPictureInPicture2,
LuVideo,
LuVideoOff,
} from "react-icons/lu";
import { import {
MdNoPhotography, MdNoPhotography,
MdPersonOff, MdPersonOff,
@ -113,20 +120,25 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
[clickOverlayRef, clickOverlay, sendPtz], [clickOverlayRef, clickOverlay, sendPtz],
); );
// fullscreen state // fullscreen / pip state
useEffect(() => { useEffect(() => {
if (mainRef.current == null) { if (mainRef.current == null) {
return; return;
} }
const listener = () => { const fsListener = () => {
setFullscreen(document.fullscreenElement != null); setFullscreen(document.fullscreenElement != null);
}; };
document.addEventListener("fullscreenchange", listener); const pipListener = () => {
setPip(document.pictureInPictureElement != null);
};
document.addEventListener("fullscreenchange", fsListener);
document.addEventListener("focusin", pipListener);
return () => { return () => {
document.removeEventListener("fullscreenchange", listener); document.removeEventListener("fullscreenchange", fsListener);
document.removeEventListener("focusin", pipListener);
}; };
}, [mainRef]); }, [mainRef]);
@ -135,6 +147,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
const [audio, setAudio] = useState(false); const [audio, setAudio] = useState(false);
const [mic, setMic] = useState(false); const [mic, setMic] = useState(false);
const [fullscreen, setFullscreen] = useState(false); const [fullscreen, setFullscreen] = useState(false);
const [pip, setPip] = useState(false);
const growClassName = useMemo(() => { const growClassName = useMemo(() => {
const aspect = camera.detect.width / camera.detect.height; const aspect = camera.detect.width / camera.detect.height;
@ -237,6 +250,23 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
}} }}
/> />
)} )}
{!isIOS && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
title={pip ? "Close" : "Picture in Picture"}
onClick={() => {
if (!pip) {
setPip(true);
} else {
document.exitPictureInPicture();
setPip(false);
}
}}
/>
)}
{window.isSecureContext && ( {window.isSecureContext && (
<CameraFeatureToggle <CameraFeatureToggle
className="p-2 md:p-0" className="p-2 md:p-0"
@ -293,6 +323,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
micEnabled={mic} micEnabled={mic}
iOSCompatFullScreen={isIOS} iOSCompatFullScreen={isIOS}
preferredLiveMode={preferredLiveMode} preferredLiveMode={preferredLiveMode}
pip={pip}
/> />
</div> </div>
{camera.onvif.host != "" && ( {camera.onvif.host != "" && (