| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 | 
| 12 | 13 | 14 | 15 | 16 | 17 | 18 | 
| 19 | 20 | 21 | 22 | 23 | 24 | 25 | 
| 26 | 27 | 28 | 29 | 30 | 31 | 
- Prism.js
- Redux
- three.js
- code editor
- jszip
- html2canvas
- append row
- Excel
- REST API
- typescript
- M3U8
- localization
- KakaoMap
- node
- Flutter
- hls.js
- multiple camera
- how to install cursor on ubuntu
- uint8array
- cursor-ubuntu-installer
- uint16array
- HLS
- Game js
- babel
- Babel standalone
- webrtc
- swagger-typescript-api
- http live streaming
- react
- segment
- Today
- Total
Never give up
React - Multiple camera recording 본문
최근 여러개의 카메라를 제어할 일이 생겼는데
한번에 모든 카메라를 보여주는 형태는 아니고
원하는 시점에 카메라를 스위칭 후 화면에 보여주고
해당 화면을 원하는 형태로 편집 후 녹화해 주는 기능이 필요했습니다

처음에 고민했던 부분은 카메라를 껏다 켯다 해보는거 였는데
media recorder n개를 사용해서 각각 녹화 후 timeline에 맞게 ffmpeg으로 붙여주는 번거로운 작업이 예상 되었고
지연이랑 플리커링 부분 그리고 만약 카메라가 원하는 시점에 안켜지면 서비스 품질이 떨어질거라 예상해 다른 방법을 선택했습니다
1. streaming은 계속 진행
2. canvas를 이용해서 카메라 보여주기
3. 시간 혹은 특정 이벤트에 따른 video 스위칭
4. canvas를 media recorder로 녹화
그럼 코드로 확인해보겠습니다
export const getMultipleMedia = async () => {
  const list = await getMultipleCameras();
  let streams: MediaStream[] = [];
  for (let i = 0; i < list.length; i++) {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: i === 0,
      video: {
        deviceId: { exact: list[i] },
        width: VIDEO_SIZE.width,
        height: VIDEO_SIZE.height,
        aspectRatio: VIDEO_SIZE.aspectRatio,
        frameRate: { ideal: 30, max: 60 },
      },
    });
    streams = [...streams, stream];
  }
  return streams;
};
const getMultipleCameras = async () => {
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    return [];
  }
  // Request permission
  const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  });
  try {
    const devices = await navigator.mediaDevices.enumerateDevices();
    // 비디오 입력 장치 (카메라) 필터링
    const videoDevices = devices.filter(
      (device) => device.kind === "videoinput"
    );
    let list: string[] = [];
    for (const e of videoDevices) {
      const label = e.label.toLowerCase();
      if (사용하는 카메라 label) {
        list = [...list, e.deviceId];
      }
    }
    return list;
  } finally {
    // Stop all tracks to turn off camera/mic
    stream.getTracks().forEach((track) => track.stop());
  }
};사용전에 먼저 permission을 가져와야되는데
기존에 사용하던 request permission은 deprecated돼서, 껏다가 켜는 형태로 진행 했습니다
그리고 사용 가능한 기기들을 검색 후, 원하는 카메라를 가져옵니다
다음으로 카메라 id를 이용해서 stream들을 불러옵니다
const useTestRecord = () => {
  const videoRefList = useRef<HTMLVideoElement[]>([]);
  const frameId = useRef<number>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [streams, setStreams] = useState<MediaStream[]>([]);
  const activeIdx = useRef<number>(0);
  const { startInterval, stopInterval } = useInterval(() => {
    activeIdx.current = (activeIdx.current + 1) % streams.length;
  }, 3000);
  console.log(videoRefList.current, activeIdx);
  const {
    videoBlob,
    setVideoBlob,
    setRecordVideo,
    startRecordingVideo,
    stopRecordingVideo,
  } = useTestVideo();
  useEffect(() => {
    init();
    commonUtil.delay(5000).then(() => {
      onStartRecording();
    });
    return () => {
      onTurnOffMedia();
    };
  }, []);
  useEffect(() => {
    onStreamMedia();
  }, [streams]);
  useEffect(() => {
    if (videoBlob) {
      downloadBlob(videoBlob, "test.webm");
    }
  }, [videoBlob]);
  const downloadBlob = (blob: Blob, filename: string) => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };
  const init = async () => {
    const streams = await getMultipleMedia();
    setStreams(streams);
  };
  const onStreamMedia = () => {
    if (streams.length === 0) {
      return;
    }
    for (let i = 0; i < streams.length; i++) {
      videoRefList.current[i].srcObject = streams[i];
    }
    videoRefList.current[0].onloadedmetadata = () => {
      drawFrame();
      startInterval();
    };
    const videoStream = canvasRef.current!.captureStream(30);
    setRecordVideo(videoStream);
  };
  const onTurnOffMedia = () => {
    for (let i = 0; i < streams.length; i++) {
      const mediaStream = streams[i];
      mediaStream.getTracks().forEach((track) => {
        track.stop();
      });
    }
    videoRefList.current.forEach((video) => {
      video.pause();
      video.srcObject = null;
    });
    stopRecordingVideo();
    stopInterval();
    setVideoBlob(null);
    if (frameId.current) {
      cancelAnimationFrame(frameId.current);
      frameId.current = null;
    }
  };
  const onStartRecording = () => {
    startRecordingVideo();
  };
  const drawFrame = () => {
    drawCanvas(canvasRef.current, videoRefList.current[activeIdx.current]);
    frameId.current = requestAnimationFrame(drawFrame);
  };
  return {
    videoRefList,
    onTurnOffMedia,
    canvasRef,
    streams,
  };
};
해당 hook에서는 canvas를 이용해서 video를 그려서 출력해주는 부분을 진행해줍니다
기본적인 video 세팅 후 interval이 지나면 active index를 바꿔주면서 canvas에 그려주는 작업을 수행합니다
drawCanvas는 여러가지 세팅이 들어갈텐데 필요한 부분만 보자면
const ctx = canvas.getContext("2d");
ctx.drawImage(video, ...기타 크기 세팅);video 태그에 출력되는 부분을 canvas에 그려주는 형태입니다
이후 해당 부분은 media recorder를 통해 record하는 부분인데
해당 코드는 예제가 많이 있을테니 생략하도록 하겠습니다
추가로 지금 현재 고민되는 포인트는
1. 여러 카메라의 스트리밍을 한 pc에서 받기에 더 나은 쿨링 솔루션이 필요
2. media tracking을 중단했다가 시작하는게 성능적으로 문제 없고, 발열제어에 도움이 될지
3. deviceId 지정 불가능 및 가변적
3번 부분은 gpt신에게 물어보니
IDs are only unstable if: unplug/replug, switch ports, or site loses camera permission.처음에 작동 시키고 별도로 코드분리, 전원 on/off, permission변경만 없으면 동일하다 합니다
그래서 시나리오로 전원켜기 -> 어드민에서 지정 -> 시작
해당 부분을 자동화할 수 있을지 확인해봐야될거 같습니다
이 부분은 프로젝트를 더 진행해가면서 맞춰봐야될거 같습니다
'WEB' 카테고리의 다른 글
| Node - appending row to Google spread sheet (0) | 2025.09.16 | 
|---|---|
| React, Next - localization with google spread sheet (0) | 2025.09.16 | 
| React - zip, unzip(Feat. JSZip) (2) | 2024.11.14 | 
| React - excel example(feat. XLSX) (1) | 2024.11.14 | 
| React - Failed to fetch dynamically imported module (0) | 2024.08.20 | 
 
								