Simplify switching between cameras

This commit is contained in:
Nicolas Mowen 2024-03-07 14:26:20 -07:00
parent 509211b440
commit 929a1b4b23
3 changed files with 127 additions and 72 deletions

View File

@ -28,6 +28,7 @@ type DynamicVideoPlayerProps = {
timeRange: { start: number; end: number };
cameraPreviews: Preview[];
previewOnly?: boolean;
preloadRecordings: boolean;
onControllerReady: (controller: DynamicVideoController) => void;
onClick?: () => void;
};
@ -37,6 +38,7 @@ export default function DynamicVideoPlayer({
timeRange,
cameraPreviews,
previewOnly = false,
preloadRecordings = true,
onControllerReady,
onClick,
}: DynamicVideoPlayerProps) {
@ -100,18 +102,26 @@ export default function DynamicVideoPlayer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller]);
const [initPreviewOnly, setInitPreviewOnly] = useState(previewOnly);
useEffect(() => {
if (!controller || !playerRef) {
return;
}
if (previewOnly == initPreviewOnly) {
return;
}
if (previewOnly) {
playerRef.autoplay(false);
controller.removePlayerListeners();
} else {
controller.setPlayerListeners();
playerRef.play();
controller.seekToTimestamp(playerRef.currentTime() || 0, true);
}
setInitPreviewOnly(previewOnly);
// we only want to fire once when players are ready
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller, previewOnly]);
@ -266,42 +276,44 @@ export default function DynamicVideoPlayer({
className={`relative ${className ?? ""} ${onClick ? (hasRecordingAtTime ? "cursor-pointer" : "") : ""}`}
onClick={onClick}
>
<div
className={`w-full relative ${
previewOnly || (currentPreview != undefined && isScrubbing)
? "hidden"
: "visible"
}`}
>
<VideoPlayer
options={{
preload: "auto",
autoplay: !previewOnly,
sources: [initialPlaybackSource],
aspectRatio: tallVideo ? "16:9" : undefined,
controlBar: {
remainingTimeDisplay: false,
progressControl: {
seekBar: false,
},
},
}}
seekOptions={{ forward: 10, backward: 5 }}
onReady={(player) => {
setPlayerRef(player);
}}
onDispose={() => {
setPlayerRef(undefined);
}}
{preloadRecordings && (
<div
className={`w-full relative ${
previewOnly || (currentPreview != undefined && isScrubbing)
? "hidden"
: "visible"
}`}
>
{config && focusedItem && (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[camera]}
/>
)}
</VideoPlayer>
</div>
<VideoPlayer
options={{
preload: "auto",
autoplay: !previewOnly,
sources: [initialPlaybackSource],
aspectRatio: tallVideo ? "16:9" : undefined,
controlBar: {
remainingTimeDisplay: false,
progressControl: {
seekBar: false,
},
},
}}
seekOptions={{ forward: 10, backward: 5 }}
onReady={(player) => {
setPlayerRef(player);
}}
onDispose={() => {
setPlayerRef(undefined);
}}
>
{config && focusedItem && (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[camera]}
/>
)}
</VideoPlayer>
</div>
)}
<video
ref={previewRef}
className={`size-full rounded-2xl ${currentPreview != undefined && (previewOnly || isScrubbing) ? "visible" : "hidden"} ${tallVideo ? "aspect-tall" : ""} bg-black`}

View File

@ -659,6 +659,7 @@ function MotionReview({
timeRange={currentTimeRange}
cameraPreviews={relevantPreviews || []}
previewOnly
preloadRecordings={false}
onControllerReady={(controller) => {
videoPlayersRef.current[camera.name] = controller;
setPlayerReady(true);

View File

@ -49,6 +49,10 @@ export function DesktopRecordingView({
return chunk.start <= startTime && chunk.end >= startTime;
}),
);
const currentTimeRange = useMemo(
() => timeRange.ranges[selectedRangeIdx],
[selectedRangeIdx, timeRange],
);
// move to next clip
useEffect(() => {
@ -59,21 +63,18 @@ export function DesktopRecordingView({
return;
}
const firstController = Object.values(videoPlayersRef.current)[0];
const mainController = videoPlayersRef.current[mainCamera];
if (firstController) {
firstController.onClipChangedEvent((dir) => {
if (
dir == "forward" &&
selectedRangeIdx < timeRange.ranges.length - 1
) {
setSelectedRangeIdx(selectedRangeIdx + 1);
} else if (selectedRangeIdx > 0) {
setSelectedRangeIdx(selectedRangeIdx - 1);
if (mainController) {
mainController.onClipChangedEvent((dir) => {
if (dir == "forward") {
if (selectedRangeIdx < timeRange.ranges.length - 1) {
setSelectedRangeIdx(selectedRangeIdx + 1);
}
}
});
}
}, [selectedRangeIdx, timeRange, videoPlayersRef, playerReady]);
}, [selectedRangeIdx, timeRange, videoPlayersRef, playerReady, mainCamera]);
// scrubbing and timeline state
@ -82,11 +83,25 @@ export function DesktopRecordingView({
useEffect(() => {
if (scrubbing) {
if (
currentTime > currentTimeRange.end + 60 ||
currentTime < currentTimeRange.start - 60
) {
const index = timeRange.ranges.findIndex(
(seg) => seg.start <= currentTime && seg.end >= currentTime,
);
if (index != -1) {
setSelectedRangeIdx(index);
}
return;
}
Object.values(videoPlayersRef.current).forEach((controller) => {
controller.scrubToTimestamp(currentTime);
});
}
}, [currentTime, scrubbing]);
}, [currentTime, scrubbing, timeRange, currentTimeRange]);
useEffect(() => {
if (!scrubbing) {
@ -99,22 +114,25 @@ export function DesktopRecordingView({
const onSelectCamera = useCallback(
(newCam: string) => {
videoPlayersRef.current[mainCamera].onPlayerTimeUpdate(undefined);
videoPlayersRef.current[mainCamera].scrubToTimestamp(currentTime);
videoPlayersRef.current[newCam].seekToTimestamp(currentTime, true);
videoPlayersRef.current[newCam].onPlayerTimeUpdate(
(timestamp: number) => {
setCurrentTime(timestamp);
const lastController = videoPlayersRef.current[mainCamera];
const newController = videoPlayersRef.current[newCam];
lastController.onPlayerTimeUpdate(undefined);
lastController.scrubToTimestamp(currentTime);
newController.onCanPlay(() => {
newController.seekToTimestamp(currentTime, true);
newController.onCanPlay(null);
});
newController.onPlayerTimeUpdate((timestamp: number) => {
setCurrentTime(timestamp);
allCameras.forEach((cam) => {
if (cam != newCam) {
videoPlayersRef.current[cam]?.scrubToTimestamp(
Math.floor(timestamp),
);
}
});
},
);
allCameras.forEach((cam) => {
if (cam != newCam) {
videoPlayersRef.current[cam]?.scrubToTimestamp(
Math.floor(timestamp),
);
}
});
});
setMainCamera(newCam);
},
[allCameras, currentTime, mainCamera],
@ -155,8 +173,9 @@ export function DesktopRecordingView({
>
<DynamicVideoPlayer
camera={cam}
timeRange={timeRange.ranges[selectedRangeIdx]}
timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []}
preloadRecordings
onControllerReady={(controller) => {
videoPlayersRef.current[cam] = controller;
setPlayerReady(true);
@ -172,7 +191,10 @@ export function DesktopRecordingView({
});
});
controller.seekToTimestamp(startTime, true);
controller.onCanPlay(() => {
controller.seekToTimestamp(startTime, true);
controller.onCanPlay(null);
});
}}
/>
</div>
@ -184,9 +206,10 @@ export function DesktopRecordingView({
<DynamicVideoPlayer
className="size-full"
camera={cam}
timeRange={timeRange.ranges[selectedRangeIdx]}
timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []}
previewOnly
preloadRecordings
onControllerReady={(controller) => {
videoPlayersRef.current[cam] = controller;
setPlayerReady(true);
@ -265,6 +288,10 @@ export function MobileRecordingView({
return chunk.start <= startTime && chunk.end >= startTime;
}),
);
const currentTimeRange = useMemo(
() => timeRange.ranges[selectedRangeIdx],
[selectedRangeIdx, timeRange],
);
// move to next clip
useEffect(() => {
@ -273,10 +300,10 @@ export function MobileRecordingView({
}
controllerRef.current.onClipChangedEvent((dir) => {
if (dir == "forward" && selectedRangeIdx < timeRange.ranges.length - 1) {
setSelectedRangeIdx(selectedRangeIdx + 1);
} else if (selectedRangeIdx > 0) {
setSelectedRangeIdx(selectedRangeIdx - 1);
if (dir == "forward") {
if (selectedRangeIdx < timeRange.ranges.length - 1) {
setSelectedRangeIdx(selectedRangeIdx + 1);
}
}
});
}, [playerReady, selectedRangeIdx, timeRange]);
@ -290,9 +317,23 @@ export function MobileRecordingView({
useEffect(() => {
if (scrubbing) {
if (
currentTime > currentTimeRange.end + 60 ||
currentTime < currentTimeRange.start - 60
) {
const index = timeRange.ranges.findIndex(
(seg) => seg.start <= currentTime && seg.end >= currentTime,
);
if (index != -1) {
setSelectedRangeIdx(index);
}
return;
}
controllerRef.current?.scrubToTimestamp(currentTime);
}
}, [currentTime, scrubbing]);
}, [currentTime, scrubbing, currentTimeRange, timeRange]);
useEffect(() => {
if (!scrubbing) {
@ -328,8 +369,9 @@ export function MobileRecordingView({
<div>
<DynamicVideoPlayer
camera={startCamera}
timeRange={timeRange.ranges[selectedRangeIdx]}
timeRange={currentTimeRange}
cameraPreviews={relevantPreviews || []}
preloadRecordings
onControllerReady={(controller) => {
controllerRef.current = controller;
setPlayerReady(true);