chrisguttandin / extendable-media-recorder

An extendable drop-in replacement for the native MediaRecorder.
MIT License
276 stars 13 forks source link

Media Recorder chunks not working after the first chunk #690

Closed Eyoel-T closed 3 months ago

Eyoel-T commented 3 months ago

I'm trying to record the webcam and audio and then want to get small chunks to send to the server and process them, but except for the first chunk, all subsequent chunks are not working.

What am I missing here?

"use client";

import React, { useState, useRef, useEffect } from "react";
import { MediaRecorder } from "extendable-media-recorder";

function MediaRecorderComponent() {
  const [isRecording, setIsRecording] = useState(false);
  const [videoDevices, setVideoDevices] = useState([]);
  const [audioDevices, setAudioDevices] = useState([]);
  const [selectedVideoDevice, setSelectedVideoDevice] = useState("");
  const [selectedAudioDevice, setSelectedAudioDevice] = useState("");
  const videoRef = useRef(null);
  const mediaRecorderRef = useRef(null);
  const recordedChunksRef = useRef([]);

  useEffect(() => {
    // Fetch available video and audio devices when the component mounts
    async function fetchDevices() {
      const devices = await navigator.mediaDevices.enumerateDevices();
      const videoInputDevices = devices.filter(
        (device) => device.kind === "videoinput"
      );
      const audioInputDevices = devices.filter(
        (device) => device.kind === "audioinput"
      );

      setVideoDevices(videoInputDevices);
      setAudioDevices(audioInputDevices);

      // Set default selected devices if available
      if (videoInputDevices.length > 0) {
        setSelectedVideoDevice(videoInputDevices[0].deviceId);
      }
      if (audioInputDevices.length > 0) {
        setSelectedAudioDevice(audioInputDevices[0].deviceId);
      }
    }

    fetchDevices();
  }, []);

  const startRecording = async () => {
    recordedChunksRef.current = [];

    // Get user media with selected devices
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        deviceId: selectedVideoDevice
          ? { exact: selectedVideoDevice }
          : undefined,
      },
      audio: {
        deviceId: selectedAudioDevice
          ? { exact: selectedAudioDevice }
          : undefined,
      },
    });

    // Set the video preview
    videoRef.current.srcObject = stream;

    // Initialize the MediaRecorder for WebM
    const mediaRecorder = new MediaRecorder(stream, {
      mimeType: "video/webm;codecs=vp9",
    });

    mediaRecorderRef.current = mediaRecorder;

    // Handle the data available event
    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        recordedChunksRef.current.push(event.data);

        // Create a blob from the chunk
        const blob = new Blob([event.data], { type: "video/webm" });

        // Create a download link
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.style.display = "none";
        a.href = url;
        a.download = `recording_${Date.now()}.webm`;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
      }
    };

    // Start recording in chunks of 3 seconds
    mediaRecorder.start(3000);

    setIsRecording(true);
  };

  const stopRecording = () => {
    mediaRecorderRef.current.stop();
    setIsRecording(false);
  };

  return (
    <div>
      <h1>Record and Download Video (WebM) Chunks</h1>

      {/* Video Device Selection */}
      <label>
        Select Video Device:
        <select
          value={selectedVideoDevice}
          onChange={(e) => setSelectedVideoDevice(e.target.value)}
          disabled={isRecording}
        >
          {videoDevices.map((device) => (
            <option key={device.deviceId} value={device.deviceId}>
              {device.label || `Camera ${device.deviceId}`}
            </option>
          ))}
        </select>
      </label>

      {/* Audio Device Selection */}
      <label>
        Select Audio Device:
        <select
          value={selectedAudioDevice}
          onChange={(e) => setSelectedAudioDevice(e.target.value)}
          disabled={isRecording}
        >
          {audioDevices.map((device) => (
            <option key={device.deviceId} value={device.deviceId}>
              {device.label || `Microphone ${device.deviceId}`}
            </option>
          ))}
        </select>
      </label>

      <div>
        <button onClick={startRecording} disabled={isRecording}>
          Start Recording
        </button>
        <button onClick={stopRecording} disabled={!isRecording}>
          Stop Recording
        </button>
      </div>

      <video
        ref={videoRef}
        autoPlay
        muted
        style={{ width: "100%", marginTop: "10px" }}
      ></video>
    </div>
  );
}

export default MediaRecorderComponent;
chrisguttandin commented 3 months ago

Unfortunately this behavior is specified by the MediaRecorder spec.

When multiple Blobs are returned (because of timeslice or requestData()), the individual Blobs need not be playable, but the combination of all the Blobs from a completed recording MUST be playable.

https://w3c.github.io/mediacapture-record/#dom-mediarecorder-start