Skip to content

Commit

Permalink
fix(stability): Improve hook state transitions (#4)
Browse files Browse the repository at this point in the history
This ensures that the functions returned from any of our hooks are referentially stable and do not change e.g. when userMedia is obtained.
  • Loading branch information
bengreenier authored Sep 11, 2024
1 parent 833a795 commit 65a5e2a
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 61 deletions.
1 change: 1 addition & 0 deletions packages/examples/src/WebcamPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
export function WebcamPreview() {
const { request, isError, error, isLoading, isReady, media } =
useMedia("user");

const {
startRecording,
stopRecording,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-user-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"name": "bengreenier",
"url": "https://github.com/bengreenier"
},
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"licenses": [
{
Expand Down
7 changes: 1 addition & 6 deletions packages/react-user-media/src/hooks/use-media-devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,6 @@ export function useMediaDevices(

const request = useCallback(
function requestMediaDevices() {
// don't request again if one is still pending
if (isLoading) {
return;
}

if (!navigator.mediaDevices.enumerateDevices) {
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
return setError(
Expand All @@ -141,7 +136,7 @@ export function useMediaDevices(
},
);
},
[isLoading, filter],
[filter],
);

useEffect(
Expand Down
88 changes: 40 additions & 48 deletions packages/react-user-media/src/hooks/use-media-recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useCallback,
useSyncExternalStore,
useEffect,
useRef,
} from "react";
import { ShallowShapeOf } from "../types";

Expand Down Expand Up @@ -160,9 +161,15 @@ export type RecorderState =
* @returns See {@link RecorderState} for more information.
*/
export function useMediaRecorder(): RecorderState {
const [recorder, setRecorder] = useState<MediaRecorder | undefined>(
undefined,
);
// we need _both_ a referentially stable version of MediaRecorder and a mutable version
// so that we can have stable start/stop functions, but also dynamic state updates
const recorderRef = useRef<MediaRecorder | null>(null);
const [isRecorderDirty, setIsRecorderDirty] = useState<boolean>(false);
const recorder = useMemo(() => {
if (isRecorderDirty) setIsRecorderDirty(false);

return recorderRef.current ?? undefined;
}, [recorderRef, isRecorderDirty]);

const recorderState = useMediaRecorderState(recorder);
const isRecording = useMemo(
Expand All @@ -182,60 +189,45 @@ export function useMediaRecorder(): RecorderState {
const [startTime, setStartTime] = useState<DOMHighResTimeStamp | null>(null);
const [endTime, setEndTime] = useState<DOMHighResTimeStamp | null>(null);

const startRecording = useCallback(
function startRecordingMedia(
media: MediaStream,
options?: RecorderOptions,
) {
// don't allow multiple recordings at once
if (isRecording) {
return;
}

const { timeslice, dataAvailableHandler, ...recorderOptions } = {
timeslice: 30 * 1000 /* 30s */,
dataAvailableHandler: (
ev: BlobEvent,
callback: (value: React.SetStateAction<Blob[]>) => void,
) => {
callback((current) => current.concat(ev.data));
},
...options,
};
const startRecording = useCallback(function startRecordingMedia(
media: MediaStream,
options?: RecorderOptions,
) {
const { timeslice, dataAvailableHandler, ...recorderOptions } = {
timeslice: 30 * 1000 /* 30s */,
dataAvailableHandler: (
ev: BlobEvent,
callback: (value: React.SetStateAction<Blob[]>) => void,
) => {
callback((current) => current.concat(ev.data));
},
...options,
};

const recorder = new MediaRecorder(media, recorderOptions);
const recorder = new MediaRecorder(media, recorderOptions);

recorder.addEventListener("dataavailable", function onDataAvailable(ev) {
dataAvailableHandler(ev, setSegments);
});
recorder.addEventListener("dataavailable", function onDataAvailable(ev) {
dataAvailableHandler(ev, setSegments);
});

setSegments([]);
setSegments([]);

const startTime = performance.now();
recorder.start(timeslice);
const startTime = performance.now();
recorder.start(timeslice);

setStartTime(startTime);
setRecorder(recorder);
},
[isRecording],
);
setStartTime(startTime);

const stopRecording = useCallback(
function stopRecordingMedia() {
// don't allow stopping of "nothing"
if (!isRecording) {
console.log("not recording");
return;
}
recorderRef.current = recorder;
setIsRecorderDirty(true);
}, []);

const endTime = performance.now();
const stopRecording = useCallback(function stopRecordingMedia() {
const endTime = performance.now();

recorder?.stop();
recorderRef.current?.stop();

setEndTime(endTime);
},
[isRecording, recorder],
);
setEndTime(endTime);
}, []);

const state = {
isError,
Expand Down
7 changes: 1 addition & 6 deletions packages/react-user-media/src/hooks/use-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,6 @@ export function useMedia<
function requestUserMedia(
...args: Parameters<inferMediaDef<TType>["requestType"]["request"]>
) {
// don't request again if one is still pending
if (isLoading) {
return;
}

if (type === "user" && !navigator.mediaDevices.getUserMedia) {
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
return setError(
Expand Down Expand Up @@ -258,7 +253,7 @@ export function useMedia<
(type) satisfies never;
}
},
[type, isLoading],
[type],
);

const state = {
Expand Down

0 comments on commit 65a5e2a

Please sign in to comment.