0

Here is a component that asks user for camera, previews video and then let's user watch it again with video controls (download, nav to specific moment, etc). The thing is the recorded video seems to have "lost" duration, meaning the <video> doesn't know how long the video lasts. How to avoid this pitfall? Also if you have better ways to code this I'm opened for any change. Thanks!

export default function RecordVideo() {
  const [state, setState] = createSignal<State>("idle");
  const [blob, setBlob] = createSignal<Blob | null>(null);
  const [count, setCount] = createSignal<number>(0);
  const [message, setMessage] = createSignal<string>("start recording");

  let preview: { srcObject: MediaStream };
  let recorder: MediaRecorder;
  let interval: NodeJS.Timer;

  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true,
      });
      setState("recording");
      preview.srcObject = stream;
      recorder = new MediaRecorder(stream);
      const chunks: Blob[] = [];
      recorder.ondataavailable = ({ data }) => chunks.push(data);
      recorder.onstop = () => {
        setBlob(new Blob(chunks, { type: "video/mp4" }));
      };
      recorder.start();
      interval = setInterval(() => setCount(prev => prev + 1), 1000);
    } catch ({ message }: any) {
      setMessage(message);
    }
  };

  const stopRecording = () => {
    preview.srcObject.getTracks().forEach(track => track.stop());
    recorder.stop();
    clearInterval(interval);
    setState("stopped");
  };

  const deleteRecording = () => {
    setBlob(null);
    setCount(0);
    setState("idle");
  };

  const downloadRecording = () =>
    Object.assign(document.createElement("a"), {
      href: URL.createObjectURL(blob()),
      download: "a.mp4",
      type: "video/mp4",
    }).click();

  onCleanup(() => {
    clearInterval(interval);
    preview.srcObject?.getTracks().forEach(track => track.stop());
  });

return (
    <section>
      <Switch>
        <Match when={state() === "idle"}>
          <div onClick={startRecording}>
            {message()}
          </div>
        </Match>
        <Match when={state() === "recording"}>
          <div>
            <video
              ref={preview}
              autoplay
              muted
            />
            <div>
              <span>{formatCount(count())}</span>
              <button onClick={stopRecording}>
                stop
              </button>
            </div>
          </div>
        </Match>
        <Match when={state() === "stopped" && blob()}>
          <div>
            <video
              src={URL.createObjectURL(blob())}
              autoplay
              controls
            />
            <div>
              <button onClick={deleteRecording}>delete</button>
              <button onClick={downloadRecording}>
                download
              </button>
            </div>
          </div>
        </Match>
      </Switch>
    </section>
  );
}
Thomas
  • 147
  • 10
  • And https://stackoverflow.com/questions/63640361/how-to-add-duration-to-metadata-of-files-recorder-by-mediarecorder – Kaiido Jul 18 '23 at 07:36

1 Answers1

-1

You have fragmented component state. When you use primitives for a complex state, one state update may make another one fallback to its previous value or overwrite its change. It is best keep the component state in a single object, so update is always consistent.

Let me elaborate using a promise as an example:

The status of a promise could be represented as:

const [status, setStatus] = createSignal('pending');
const [data, setData] = createSignal(undefined);
const [error, setError] = createSignal(undefined);

When we send the request, status will be 'pending', data and error will be null. When we receive data, status should be 'resolved', data is whatever we receive and error is null.

There are three signals to be updated, and they should be updated at once, if we do it one by one, we will have inconsistent state at some point.

setStatus('resolved');
setData(dataWeReceived);

There is no way to avoid it unless you use batch:

batch(() => {
  setState('resolved');
  setData(dataWeReceived);
});

Batch delays running the effects until states updates are completed. So, effects do not receive inconsistent states.

https://www.solidjs.com/docs/latest/api#batch

Alternatively we could use a single object:

type Resource<T = any, E = Error> = 
| { status: 'pending' }
| { status: 'success', success: T }
| { status: 'error', error: E }
;

const [state, setState] = createSignal({ status: 'pending' })

Now when we update the sate we update a single object, so the state is always consistent.

setState({ status: 'resolved', data: dataWeReceived )});

You can see the discussions for an actual case:

So, to answer your question, all these states:

const [state, setState] = createSignal<State>("idle");
const [blob, setBlob] = createSignal<Blob | null>(null);
const [count, setCount] = createSignal<number>(0);
const [message, setMessage] = createSignal<string>("start recording");

Should be merged into a single object:

const [state, setState] = createSignal({
  status: 'idle',
  count: 0,
  blob: null,
  message: "Start Recording",
});

When you need to update one or multiple properties:

setState(prev => ({...prev, count: prev + 1 });

Alternatively you can use a store, which internally uses a single proxied object. Store is a wrapper around a reactive object that captures all the interaction and re-wires. But performance will degrade because of indirection. Signal with an object is better fit for use case.

If you want to keep the primitive states, then make sure to wrap occurring updates in a batch, so that you don't suffer from inconsistencies.

snnsnn
  • 10,486
  • 4
  • 39
  • 44
  • I'm not the person who downvoted this answer. If you could please show me the best way to deal with this, I would happily accept your answer. – Thomas Jul 18 '23 at 08:13
  • @Thomas Updated the answer. – snnsnn Jul 18 '23 at 09:40
  • I added it here: `recorder.onstop = () => { batch(() => { setBlob(new Blob(chunks, { type: "video/mp4" })); }); }; `. I'm afraid the behavior hasn't changed. Maybe you could please incorporate your answer in my code ? – Thomas Jul 18 '23 at 09:48
  • Use an object, you have state all over the component, there is no way a single batch can solve your problem. – snnsnn Jul 18 '23 at 09:52
  • please @snnsnn I'm sure I would understand immediately if you could please incorporate your answer in my code. – Thomas Jul 18 '23 at 09:53
  • Updated the answer once more, now you need to update your component in a way to use state and setState. This is clear and more maintainable. – snnsnn Jul 18 '23 at 10:00
  • I have merged all states into one but the issue persists – Thomas Jul 18 '23 at 10:13
  • Sorry thats all i can. It seems problem is not caused by Solid. Maybe you can try tracking duration explicitly by storing it in your state. – snnsnn Jul 18 '23 at 10:30
  • maybe you haven't understood the issue ? My issue is simply that the recorded video seems to have a duration (should be in its metadata?) – Thomas Jul 18 '23 at 11:12
  • it's a chrome bug. this is scandalous though. It's been 7 years and still the bug is here!!!! – Thomas Jul 18 '23 at 11:39