Handle error when live view stalls (#11665)

* Handle error when live view stalls

* Manually calculate buffer timeout

* Formatting
This commit is contained in:
Nicolas Mowen 2024-05-31 07:52:42 -06:00 committed by GitHub
parent a3d116e70e
commit 758df09da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 83 additions and 4 deletions

View File

@ -8,7 +8,11 @@ import JSMpegPlayer from "./JSMpegPlayer";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { useCameraActivity } from "@/hooks/use-camera-activity"; import { useCameraActivity } from "@/hooks/use-camera-activity";
import { LivePlayerMode, VideoResolutionType } from "@/types/live"; import {
LivePlayerError,
LivePlayerMode,
VideoResolutionType,
} from "@/types/live";
import useCameraLiveMode from "@/hooks/use-camera-live-mode"; import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import Chip from "../indicators/Chip"; import Chip from "../indicators/Chip";
@ -30,6 +34,7 @@ type LivePlayerProps = {
autoLive?: boolean; autoLive?: boolean;
onClick?: () => void; onClick?: () => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>; setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onError?: (error: LivePlayerError) => void;
}; };
export default function LivePlayer({ export default function LivePlayer({
@ -47,6 +52,7 @@ export default function LivePlayer({
autoLive = true, autoLive = true,
onClick, onClick,
setFullResolution, setFullResolution,
onError,
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
@ -145,6 +151,7 @@ export default function LivePlayer({
onPlaying={() => setLiveReady(true)} onPlaying={() => setLiveReady(true)}
pip={pip} pip={pip}
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
onError={onError}
/> />
); );
} else { } else {

View File

@ -1,5 +1,5 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { VideoResolutionType } from "@/types/live"; import { LivePlayerError, VideoResolutionType } from "@/types/live";
import { import {
SetStateAction, SetStateAction,
useCallback, useCallback,
@ -17,6 +17,7 @@ type MSEPlayerProps = {
pip?: boolean; pip?: boolean;
onPlaying?: () => void; onPlaying?: () => void;
setFullResolution?: React.Dispatch<SetStateAction<VideoResolutionType>>; setFullResolution?: React.Dispatch<SetStateAction<VideoResolutionType>>;
onError?: (error: LivePlayerError) => void;
}; };
function MSEPlayer({ function MSEPlayer({
@ -27,6 +28,7 @@ function MSEPlayer({
pip = false, pip = false,
onPlaying, onPlaying,
setFullResolution, setFullResolution,
onError,
}: MSEPlayerProps) { }: MSEPlayerProps) {
const RECONNECT_TIMEOUT: number = 30000; const RECONNECT_TIMEOUT: number = 30000;
@ -45,6 +47,7 @@ function MSEPlayer({
const [wsState, setWsState] = useState<number>(WebSocket.CLOSED); const [wsState, setWsState] = useState<number>(WebSocket.CLOSED);
const [connectTS, setConnectTS] = useState<number>(0); const [connectTS, setConnectTS] = useState<number>(0);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
@ -308,7 +311,34 @@ function MSEPlayer({
onPlaying?.(); onPlaying?.();
}} }}
muted={!audioEnabled} muted={!audioEnabled}
onError={() => { onProgress={
onError != undefined
? () => {
if (videoRef.current?.paused) {
return;
}
if (bufferTimeout) {
clearTimeout(bufferTimeout);
setBufferTimeout(undefined);
}
setBufferTimeout(
setTimeout(() => {
onError("stalled");
}, 3000),
);
}
: undefined
}
onError={(e) => {
if (
// @ts-expect-error code does exist
e.target.error.code == MediaError.MEDIA_ERR_NETWORK
) {
onError?.("startup");
}
if (wsRef.current) { if (wsRef.current) {
wsRef.current.close(); wsRef.current.close();
wsRef.current = null; wsRef.current = null;

View File

@ -1,4 +1,5 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { LivePlayerError } from "@/types/live";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
type WebRtcPlayerProps = { type WebRtcPlayerProps = {
@ -10,6 +11,7 @@ type WebRtcPlayerProps = {
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; pip?: boolean;
onPlaying?: () => void; onPlaying?: () => void;
onError?: (error: LivePlayerError) => void;
}; };
export default function WebRtcPlayer({ export default function WebRtcPlayer({
@ -21,6 +23,7 @@ export default function WebRtcPlayer({
iOSCompatFullScreen = false, iOSCompatFullScreen = false,
pip = false, pip = false,
onPlaying, onPlaying,
onError,
}: WebRtcPlayerProps) { }: WebRtcPlayerProps) {
// metadata // metadata
@ -32,6 +35,7 @@ export default function WebRtcPlayer({
const pcRef = useRef<RTCPeerConnection | undefined>(); const pcRef = useRef<RTCPeerConnection | undefined>();
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
const PeerConnection = useCallback( const PeerConnection = useCallback(
async (media: string) => { async (media: string) => {
@ -198,11 +202,39 @@ export default function WebRtcPlayer({
playsInline playsInline
muted={!audioEnabled} muted={!audioEnabled}
onLoadedData={onPlaying} onLoadedData={onPlaying}
onProgress={
onError != undefined
? () => {
if (videoRef.current?.paused) {
return;
}
if (bufferTimeout) {
clearTimeout(bufferTimeout);
setBufferTimeout(undefined);
}
setBufferTimeout(
setTimeout(() => {
onError("stalled");
}, 3000),
);
}
: undefined
}
onClick={ onClick={
iOSCompatFullScreen iOSCompatFullScreen
? () => setiOSCompatControls(!iOSCompatControls) ? () => setiOSCompatControls(!iOSCompatControls)
: undefined : undefined
} }
onError={(e) => {
if (
// @ts-expect-error code does exist
e.target.error.code == MediaError.MEDIA_ERR_NETWORK
) {
onError?.("startup");
}
}}
/> />
); );
} }

View File

@ -30,3 +30,5 @@ export type LiveStreamMetadata = {
producers: LiveProducerMetadata[]; producers: LiveProducerMetadata[];
consumers: LiveConsumerMetadata[]; consumers: LiveConsumerMetadata[];
}; };
export type LivePlayerError = "stalled" | "startup";

View File

@ -191,6 +191,7 @@ export default function LiveCameraView({
const [audio, setAudio] = useState(false); const [audio, setAudio] = useState(false);
const [mic, setMic] = useState(false); const [mic, setMic] = useState(false);
const [pip, setPip] = useState(false); const [pip, setPip] = useState(false);
const [lowBandwidth, setLowBandwidth] = useState(false);
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({ const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
width: 0, width: 0,
@ -202,8 +203,14 @@ export default function LiveCameraView({
return "webrtc"; return "webrtc";
} }
if (lowBandwidth) {
return "jsmpeg";
}
return "mse"; return "mse";
}, [mic]); }, [lowBandwidth, mic]);
// layout state
const windowAspectRatio = useMemo(() => { const windowAspectRatio = useMemo(() => {
return windowWidth / windowHeight; return windowWidth / windowHeight;
@ -419,6 +426,7 @@ export default function LiveCameraView({
pip={pip} pip={pip}
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
containerRef={containerRef} containerRef={containerRef}
onError={() => setLowBandwidth(true)}
/> />
</div> </div>
{camera.onvif.host != "" && ( {camera.onvif.host != "" && (