Skip to content

Commit

Permalink
Merge pull request #475 from charlielee/device-listener
Browse files Browse the repository at this point in the history
Refactor capture provider
  • Loading branch information
charlielee committed Nov 28, 2023
2 parents 52b0d72 + 173b897 commit 88b5e0b
Show file tree
Hide file tree
Showing 21 changed files with 213 additions and 184 deletions.
12 changes: 4 additions & 8 deletions src/common/FileRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ export const makeFrameFileRef = (trackItemId: TrackItemId, location: string): Fi
available: true,
});

export const getFileRefById = (fileRefs: FileRef[], trackItemId: TrackItemId): FileRef => {
const fileRef = fileRefs.find((fileRef) => fileRef.trackItemId === trackItemId);
if (fileRef !== undefined) {
return fileRef;
} else {
throw `No file ref with trackItemId ${trackItemId} found`;
}
};
export const getFileRefById = (
fileRefs: FileRef[],
trackItemId: TrackItemId
): FileRef | undefined => fileRefs.find((fileRef) => fileRef.trackItemId === trackItemId);
1 change: 0 additions & 1 deletion src/common/project/Take.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ export interface Take {
takeNumber: number;
frameRate: FrameRate;
holdFrames: FrameCount;
lastExportedFrameNumber: number;
frameTrack: Track;
}
3 changes: 2 additions & 1 deletion src/common/project/TrackItem.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FrameCount, TrackGroupId, TrackItemId } from "../Flavors";
import { FrameCount, TimelineIndex, TrackGroupId, TrackItemId } from "../Flavors";

export interface TrackItem {
id: TrackItemId;
length: FrameCount;
filePath: string;
fileNumber: TimelineIndex;
trackGroupId: TrackGroupId;
}
1 change: 0 additions & 1 deletion src/common/testConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const TAKE: Take = {
takeNumber: 1,
frameRate: 15,
holdFrames: 1,
lastExportedFrameNumber: 0,
frameTrack: {
id: uuidv4(),
fileType: FileRefType.FRAME,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { Action, ThunkDispatch } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../redux/store";
import { setCurrentDeviceFromId } from "../../../redux/thunks";
import InputGroup from "../../common/Input/InputGroup/InputGroup";
import InputLabel from "../../common/Input/InputLabel/InputLabel";
import InputSelect from "../../common/Input/InputSelect/InputSelect";
import SidebarBlock from "../../common/SidebarBlock/SidebarBlock";
import useDeviceList from "../../../hooks/useDeviceList";
import { ImagingDeviceIdentifier } from "../../../services/imagingDevice/ImagingDevice";
import { changeDevice, closeDevice } from "../../../redux/slices/captureSlice";

const CaptureSidebarBlock = (): JSX.Element => {
const dispatch: ThunkDispatch<RootState, void, Action> = useDispatch();
const { deviceStatus, deviceList } = useSelector((state: RootState) => ({
const { deviceStatus } = useSelector((state: RootState) => ({
deviceStatus: state.capture.deviceStatus,
deviceList: state.capture.deviceList,
}));
const deviceList = useDeviceList();

const buildCameraSourceOptions = () => ({
"No camera selected": "#",
...Object.fromEntries(deviceList.map((identifier) => [identifier.name, identifier.deviceId])),
});

const deviceIdToDeviceIdentifier = (deviceId: string): ImagingDeviceIdentifier => {
const identifier = deviceList.find((identifier) => identifier.deviceId === deviceId);
if (identifier === undefined) {
throw "Selected device not found in device list";
}
return identifier;
};

const getCurrentDeviceIdentifier = (deviceId: string) =>
deviceId === "#" ? undefined : deviceIdToDeviceIdentifier(deviceId);

return (
<SidebarBlock title="Capture">
<InputGroup>
Expand All @@ -27,9 +40,10 @@ const CaptureSidebarBlock = (): JSX.Element => {
id="camera-source-select"
options={buildCameraSourceOptions()}
value={deviceStatus?.identifier?.deviceId ?? "#"}
onChange={(deviceId) =>
dispatch(setCurrentDeviceFromId(deviceId === "#" ? undefined : deviceId))
}
onChange={(deviceId) => {
const identifier = getCurrentDeviceIdentifier(deviceId);
dispatch(identifier ? changeDevice(identifier) : closeDevice());
}}
/>
</InputGroup>
</SidebarBlock>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/animator/Preview/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const Preview = ({ device, liveViewVisible, timelineIndex }: PreviewProps): JSX.

const highlightedTrackItem = getHighlightedTrackItem(take.frameTrack, timelineIndex);
const previewSrc = highlightedTrackItem
? getFileRefById(fileRefs, highlightedTrackItem.id).location
? getFileRefById(fileRefs, highlightedTrackItem.id)?.location
: undefined;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const TimelineTrack = ({
return (
<TimelineTrackItem
title={getTrackItemTitle(track, i)}
dataUrl={getFileRefById(fileRefs, trackItem.id).location}
dataUrl={getFileRefById(fileRefs, trackItem.id)?.location}
highlighted={highlightedTrackItem?.id === trackItem.id}
key={trackItem.id}
onClick={() => onClickItem(i)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
height: 100%;
min-width: calc(var(--width-16) * 7);
width: calc(var(--width-16) * 7);
border-radius: var(--margin-025);
}

.timeline-track-item--loading {
background-color: var(--ba-dark-mid-hover);
cursor: progress;
}

.timeline-track-item__img {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "./TimelineTrackItem.css";

interface TimelineTrackItemProps {
title: string;
dataUrl: string;
dataUrl: string | undefined;
highlighted: boolean;
onClick: () => void;
}
Expand All @@ -21,14 +21,21 @@ const TimelineTrackItem = ({ title, dataUrl, highlighted, onClick }: TimelineTra
);

return (
<div className="timeline-track-item" onClick={onClick} title={title}>
<img
className={classNames("timeline-track-item__img", {
"timeline-track-item__img--highlighted": highlighted,
})}
src={dataUrl}
key={dataUrl}
/>
<div
className={classNames("timeline-track-item", {
"timeline-track-item--loading": dataUrl === undefined,
})}
onClick={onClick}
title={title}
>
{dataUrl !== undefined && (
<img
className={classNames("timeline-track-item__img", {
"timeline-track-item__img--highlighted": highlighted,
})}
src={dataUrl}
/>
)}
<div className="timeline-track-item__cover"></div>
<div className="timetime-track-item__scroll-target" ref={scrollTargetRef}></div>
</div>
Expand Down
8 changes: 1 addition & 7 deletions src/renderer/components/common/AppListener/AppListener.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router-dom";
import { PageRoute } from "../../../../common/PageRoute";
import { RootState } from "../../../redux/store";
import { fetchAndSetDeviceList, loadSavedPreferences, onRouteChange } from "../../../redux/thunks";
import { loadSavedPreferences, onRouteChange } from "../../../redux/thunks";
import { handleOnCloseButtonClick } from "../../../services/appListener/AppListenerService";
import { onDeviceChange } from "../../../services/imagingDevice/ImagingDevice";
import * as rLogger from "../../../services/rLogger/rLogger";

const AppListeners = (): JSX.Element => {
Expand All @@ -18,11 +17,6 @@ const AppListeners = (): JSX.Element => {

useEffect(() => {
dispatch(loadSavedPreferences());

dispatch(fetchAndSetDeviceList());
onDeviceChange(() => {
dispatch(fetchAndSetDeviceList());
});
}, [dispatch]);

// Handle pressing the close button
Expand Down
130 changes: 53 additions & 77 deletions src/renderer/context/CaptureContext/CaptureContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@ import { ReactNode, useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { makeFrameFileRef } from "../../../common/FileRef";
import cameraSound from "../../audio/camera.wav";
import { closeDevice } from "../../redux/slices/captureSlice";
import {
addFileRef,
addFrameTrackItem,
incrementExportedFrameNumber,
} from "../../redux/slices/projectSlice";
import useProjectAndTake from "../../hooks/useProjectAndTake";
import { addFileRef, addFrameTrackItem } from "../../redux/slices/projectSlice";
import { RootState } from "../../redux/store";
import { setCurrentDeviceFromId, updateCameraAccessStatus, withLoader } from "../../redux/thunks";
import { saveBlobToDisk } from "../../services/blobUtils/blobUtils";
import {
deviceIdentifierToDevice,
ImagingDevice,
deviceIdentifierToDevice,
} from "../../services/imagingDevice/ImagingDevice";
import { makeFrameFilePath, makeFrameTrackItem } from "../../services/project/projectBuilder";
import * as rLogger from "../../services/rLogger/rLogger";
import CaptureContext from "./CaptureContext";
import useProjectAndTake from "../../hooks/useProjectAndTake";
import * as rLogger from "../../services/rLogger/rLogger";
import useDeviceList from "../../hooks/useDeviceList";
import { closeDevice } from "../../redux/slices/captureSlice";
import { zeroPad } from "../../../common/utils";
import { TrackItem } from "../../../common/project/TrackItem";
import { getNextFileNumber } from "../../services/project/projectCalculator";

interface CaptureContextProviderProps {
children: ReactNode;
Expand All @@ -29,97 +28,74 @@ const CaptureContextProvider = ({ children }: CaptureContextProviderProps) => {
const [device, setDevice] = useState<ImagingDevice | undefined>(undefined);

const dispatch: ThunkDispatch<RootState, void, Action> = useDispatch();
const deviceList = useDeviceList();
const { project, take } = useProjectAndTake();
const { playCaptureSound, deviceStatus, deviceList } = useSelector((state: RootState) => ({
playCaptureSound: state.app.userPreferences.playCaptureSound,
deviceStatus: state.capture.deviceStatus,
deviceList: state.capture.deviceList,
}));

const onChangeDevice = useCallback(
(deviceId: string | undefined) => {
rLogger.info("captureContextProvider.onChangeDevice");
dispatch(closeDevice());
const identifier = dispatch(setCurrentDeviceFromId(deviceId));

if (identifier) {
setDevice(deviceIdentifierToDevice(identifier));
} else {
setDevice(undefined);
}
},
[dispatch]
const deviceStatus = useSelector((state: RootState) => state.capture.deviceStatus);
const playCaptureSound = useSelector(
(state: RootState) => state.app.userPreferences.playCaptureSound
);

const onOpenDevice = useCallback(
() =>
dispatch(
withLoader("Loading device", async () => {
rLogger.info("captureContextProvider.onOpenDevice");
if (!device) {
return;
}

const hasCameraAccess = await dispatch(updateCameraAccessStatus());
const deviceOpened = hasCameraAccess && (await device.open());

if (!deviceOpened) {
dispatch(closeDevice());
}
})
),
[device, dispatch]
);

const onCloseDevice = useCallback(() => {
rLogger.info("captureContextProvider.onCloseDevice");
device?.close();
}, [device]);

const takePhoto = async () => {
if (!device) {
return;
}
const takePhoto = () => {
rLogger.info("captureContextProvider.takePhoto");

if (playCaptureSound) {
const audio = new Audio(cameraSound);
audio.play();
}

const filePath = makeFrameFilePath(project, take);
// Frame track items should be created synchronously to ensure frames are created in the correct order
// and do not have overwriting file names
const fileNumber = getNextFileNumber(take.frameTrack);
const filePath = makeFrameFilePath(project, take, zeroPad(fileNumber, 5));
const trackItem = makeFrameTrackItem(filePath, fileNumber);
dispatch(addFrameTrackItem(trackItem));

// Intentionally fire async method without await
processPhoto(filePath, trackItem);
};

const processPhoto = async (filePath: string, trackItem: TrackItem) => {
if (!device) {
return;
}
const imageData = await device.takePhoto();
saveBlobToDisk(filePath, imageData);

const trackItem = makeFrameTrackItem(filePath);
const imageUrl = URL.createObjectURL(imageData);
dispatch(addFileRef(makeFrameFileRef(trackItem.id, imageUrl)));
dispatch(addFrameTrackItem(trackItem));
dispatch(incrementExportedFrameNumber());
};

useEffect(() => {
if (deviceStatus?.identifier !== device?.identifier) {
onChangeDevice(deviceStatus?.identifier?.deviceId);
const onChangeDevice = useCallback(async () => {
rLogger.info("captureContextProvider.onChangeDevice", JSON.stringify(deviceStatus));
const identifier = deviceStatus?.identifier;
const newDevice = identifier ? deviceIdentifierToDevice(identifier) : undefined;

device?.close();

if (deviceStatus?.open === true) {
await newDevice?.open();
}
}, [device?.identifier, deviceStatus?.identifier, onChangeDevice]);

useEffect(() => {
setDevice(newDevice);
}, [deviceStatus]); // eslint-disable-line react-hooks/exhaustive-deps

const onDeviceListChange = useCallback(() => {
if (
deviceStatus &&
!deviceList.find((device) => device.deviceId === deviceStatus.identifier.deviceId)
!deviceList.find((identifier) => identifier.deviceId === deviceStatus.identifier.deviceId)
) {
rLogger.info("captureContextProvider.currentDeviceRemoved");
onChangeDevice(undefined);
rLogger.info("captureContextProvider.currentDeviceDisconnected");
dispatch(closeDevice());
}
}, [deviceList, deviceStatus, onChangeDevice]);
}, [deviceList, deviceStatus, dispatch]);

useEffect(() => {
if (deviceStatus && deviceStatus?.open) {
onOpenDevice();
} else {
onCloseDevice();
}
}, [deviceStatus, onCloseDevice, onOpenDevice]);
onChangeDevice();
}, [onChangeDevice, deviceStatus]);

useEffect(() => {
onDeviceListChange();
}, [onDeviceListChange, deviceList]);

return (
<CaptureContext.Provider
Expand Down
28 changes: 28 additions & 0 deletions src/renderer/hooks/useDeviceList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import {
ImagingDeviceIdentifier,
addDeviceChangeListeners,
listDevices,
removeDeviceChangeListeners,
} from "../services/imagingDevice/ImagingDevice";

const useDeviceList = () => {
const [deviceList, setDeviceList] = useState<ImagingDeviceIdentifier[]>([]);

const fetchDeviceList = async () => {
setDeviceList(await listDevices());
};

useEffect(() => {
fetchDeviceList();
addDeviceChangeListeners(fetchDeviceList);

return () => {
removeDeviceChangeListeners(fetchDeviceList);
};
}, []);

return deviceList;
};

export default useDeviceList;

0 comments on commit 88b5e0b

Please sign in to comment.