IT/React

React 웹캠 - 4. Select webcam

루벤초이 2021. 6. 21. 23:20

React 웹캠 시리즈입니다.

Sample Code


두 개 이상의 웹캠(혹은 마이크)을 사용하는 경우 브라우저 메뉴를 통해 웹캠을 선택하는 React 앱을 만들어 봅시다.

 

navigator.mediaDevices.enumerateDevices()

오늘의 주요 API는 enumerateDevices() 인데요, 이는 사용 가능한 모든 디바이스 배열을 리턴해줍니다. 리턴 값을 console.log로 찍어 보면 아래와 같습니다.

enumerateDevices() 결과값의 예

deviceId는 이 디바이스에 접근하기 위한 ID이고  kind는 디바이스의 종류로서 audioinput은 마이크, videoinput은 카메라, audiooutput은 스피커입니다. label은 이름인데, deviceId가 사람이 알아보기 힘들기 때문에 label을 GUI에 활용합니다.

 

이렇게 얻은 deviceId를 navigator.mediaDevices.getUserMedia의 파라미터로 넣어주면 해당 디바이스의 영상/음성 스트림을 얻게 됩니다. 물론 최초에는 사용자에게 권한 요청을 하게 되지요.

{ 
  video: { deviceId: { exact: videoDeviceId } } 
  audio: { deviceId: { exact: audioDeviceId } },
}

파라미터에는 deviceId뿐만 아니라 영상/음성 입력의 속성도 조정할 수 있는데요, 가령 카메라의 Pan(좌우 회전), Tilt(상하 회전), Zoom(줌인/아웃) 기능을 사용할 수도 있고 마이크의 경우에는 음소거(mute) 기능을 컨트롤할 수 있습니다. 아래 소스 분석에서 자세히 살펴보겠습니다.

 

소스 분석

코드를 살펴봅시다.

import React, { useEffect, useRef, useState, Fragment } from 'react'
import style from './styles.module.css';

const PAN = 'pan';
const TILT = 'tilt';
const ZOOM = 'zoom';
const FEATURES = [PAN, TILT, ZOOM];

export const WebcamComponent = (props) => {
  const [camFeatures, setCamFeatures] = useState({ pan: false, tilt: false, zoom: false });
  const [stream, setStream] = useState(undefined);
  const [videoWH, setVideoWH] = useState(undefined);
  const [enableAudio, setEnableAudio] = useState(true);

  const refVideo = useRef(null);
  const refSelectMic = useRef(null);
  const refSelectVideo = useRef(null);

  const listDevices = async () => {
    const devices = await navigator.mediaDevices.enumerateDevices();

    devices.forEach((device) => {
      const option = document.createElement('option');
      option.value = device.deviceId;
      switch (device.kind) {
        case 'audioinput':
          option.text = device.label || `mic ${refSelectMic.current.length + 1}`;
          refSelectMic.current.appendChild(option);
          break;
        case 'videoinput':
          option.text = device.label || `camera ${refSelectVideo.current.length + 1}`;
          refSelectVideo.current.appendChild(option);
          break;
        default:
          console.log('Skipped Device: ' + device.kind, device && device.label);
          break;
      }
    });
  }

  const getParams = (video, audio) => {
    return {
      video: {
        deviceId: video ? { exact: video } : undefined,
        pan: true,
        tilt: true,
        zoom: true
      },
      audio: {
        deviceId: audio ? { exact: audio } : undefined,
        options: {
          muted: true,
          mirror: true
        }
      }
    }
  }

  const startWebcam = async () => {
    const stream = await navigator.mediaDevices.getUserMedia(getParams(refSelectVideo.current.value, refSelectMic.current.value));

    refVideo.current.srcObject = stream;

    const track = stream.getVideoTracks()[0];
    const capabilities = track.getCapabilities();
    const settings = track.getSettings();

    const r = {};
    FEATURES.forEach(feature => {
      r[feature] = feature in settings;

      if (r[feature]) {
        const input = document.querySelector(`input[name=${feature}]`);
        input.min = capabilities[feature].min;
        input.max = capabilities[feature].max;
        input.step = capabilities[feature].step;
        input.value = settings[feature];
        input.disabled = false;
        input.oninput = async () => {
          try {
            const constraints = { advanced: [{ [feature]: input.value }] };
            await track.applyConstraints(constraints);
          } catch (err) {
            console.log(err);
          }
        };
      }
    });
    setCamFeatures(r);

    setStream(stream);
    setVideoWH('w: ' + settings.width + ' h: ' + settings.height);
    props.onStream && props.onStream(stream);
  }

  useEffect(() => {
    listDevices();
    startWebcam();
  }, []);

  useEffect(() => {
    if (stream) {
      stream.getAudioTracks()[0].enabled = enableAudio;
    }
  }, [enableAudio])

  const getController = (feature) => {
    let enabled = camFeatures[feature];
    return (<div style={{ display: enabled ? 'inline-block' : 'none' }}>
      {feature}: <input name={feature} type="range" disabled />
    </div>)
  }

  const onMute = () => {
    setEnableAudio(!enableAudio);
    props.onMute && props.onMute(enableAudio);
  }

  return (<>
    <div style={{ fontSize: '1em' }}>
      <h3>Settings</h3>
      <div className={style.gridContainer}>
        <div className={style.gridItem}>Mic: </div>
        <div className={style.gridItem}>
          <span><select ref={refSelectMic} onChange={(e) => startWebcam()}></select></span>
          {props.onMute && <button onClick={onMute} style={{ marginLeft: '2em' }}>{enableAudio ? 'MUTE' : 'UNMUTE'}</button>}
        </div>
        <div className={style.gridItem}>Video: </div>
        <div className={style.gridItem}><select ref={refSelectVideo} onChange={(e) => startWebcam()}></select></div>
      </div>
      <div>
        {getController(PAN)}
        {getController(TILT)}
        {getController(ZOOM)}
      </div>
    </div>

    <hr />
    <video ref={refVideo} autoPlay style={{ width: '20vw', margin: 'auto' }} />
    <p style={{ fontSize: '0.8em' }}>{videoWH}</p>

    {props.audioTest &&
      <>
        <hr />
        <p>Audio Test:</p>
        <iframe
          width="100"
          height="100"
          src="https://www.youtube.com/embed/1Hkc_2b03jw"
          title="Audio Test"
          frameBorder="0"
          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" />
      </>
    }
  </>
  );
}

 

자, 이제 실행해 봅시다. 프라이버시 보호를 위한 얼굴 대신 손!

실행 결과

 

728x90
반응형

'IT > React' 카테고리의 다른 글

OBS Studio를 이용한 Zoom/WebEx 웹앱 스트리밍  (0) 2021.08.09
React Troubleshoots  (0) 2021.07.17
React 실습 - Class vs. Hook  (0) 2021.04.01
React 웹캠 - 3. Canvas  (0) 2021.03.31
React 웹캠 - 2. getUserMedia  (0) 2021.03.30