사용자로부터 이미지 캡처

대부분의 브라우저는 사용자의 카메라에 액세스할 수 있습니다.

매트 저울

이제 대부분의 브라우저에서 사용자의 동영상 및 오디오 입력에 액세스할 수 있습니다. 그러나 브라우저에 따라 완전한 동적 인라인 환경이거나 사용자 기기의 다른 앱에 위임될 수 있습니다. 무엇보다도 모든 기기에 카메라가 있는 것은 아닙니다. 그러면 어디에서나 잘 작동하는 사용자 제작 이미지를 사용하는 환경을 만들려면 어떻게 해야 할까요?

단순하게 점진적으로 시작

경험을 점진적으로 개선하려면 어디서나 작동하는 것부터 시작해야 합니다. 가장 쉬운 방법은 사용자에게 사전 녹음된 파일을 요청하는 것입니다.

URL 요청

가장 잘 지원되지만 만족도가 가장 낮은 옵션입니다. 사용자가 URL을 제공해 달라고 요청한 다음 이를 사용합니다. 이미지를 표시하기 위해서라면 어디서나 작동합니다. img 요소를 만들고 src를 설정하면 완료됩니다.

하지만 어떤 식으로든 이미지를 조작하고 싶다면 조금 더 복잡합니다. CORS는 서버에서 적절한 헤더를 설정하고 개발자가 이미지를 교차 출처로 표시하지 않는 한 실제 픽셀에 액세스하지 못하게 합니다. 이를 위한 유일한 실질적인 방법은 프록시 서버를 실행하는 것입니다.

파일 입력

이미지 파일만 원하는 것을 나타내는 accept 필터를 포함한 간단한 파일 입력 요소를 사용할 수도 있습니다.

<input type="file" accept="image/*" />

이 방법은 모든 플랫폼에서 사용할 수 있습니다. 데스크톱에서는 사용자에게 파일 시스템의 이미지 파일을 업로드하라는 메시지가 표시됩니다. iOS 및 Android의 Chrome 및 Safari에서 이 방법을 사용하면 사용자가 카메라로 직접 사진을 찍거나 기존 이미지 파일을 선택하는 옵션을 포함하여 이미지를 캡처하는 데 사용할 앱을 선택할 수 있습니다.

두 가지 옵션이 있는 Android 메뉴: 이미지 및 파일 캡처 세 가지 옵션이 있는 iOS 메뉴(사진 찍기, 사진 라이브러리, iCloud)

그러면 데이터를 <form>에 연결하거나 입력 요소에서 onchange 이벤트를 수신 대기하고 이벤트 targetfiles 속성을 읽어 자바스크립트로 조작할 수 있습니다.

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

files 속성은 FileList 객체로, 나중에 더 자세히 살펴보겠습니다.

원하는 경우 요소에 capture 속성을 추가할 수도 있습니다. 이는 카메라에서 이미지를 가져오는 것을 선호함을 브라우저에 나타냅니다.

<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />

값 없이 capture 속성을 추가하면 브라우저에서 사용할 카메라를 결정하도록 하고 "user""environment" 값은 각각 전면 카메라와 후면 카메라를 선호하도록 브라우저에 지시합니다.

capture 속성은 Android와 iOS에서 작동하지만 데스크톱에서는 무시됩니다. 그러나 Android에서는 사용자가 더 이상 기존 사진을 선택할 수 없다는 점에 유의하세요. 대신 시스템 카메라 앱이 바로 시작됩니다.

드래그 앤 드롭

이미 파일 업로드 기능을 추가하는 경우 몇 가지 간단한 방법으로 사용자 환경을 조금 더 풍부하게 만들 수 있습니다.

첫 번째는 사용자가 데스크톱이나 다른 애플리케이션에서 파일을 드래그할 수 있는 드롭 타겟을 페이지에 추가하는 것입니다.

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

파일 입력과 마찬가지로 drop 이벤트의 dataTransfer.files 속성에서 FileList 객체를 가져올 수 있습니다.

dragover 이벤트 핸들러를 사용하면 dropEffect 속성을 사용하여 사용자가 파일을 드롭할 때 발생할 결과를 알 수 있습니다.

드래그 앤 드롭은 오랫동안 사용되어 왔으며 주요 브라우저에서 잘 지원됩니다.

클립보드에서 붙여넣기

기존 이미지 파일을 가져오는 마지막 방법은 클립보드를 이용하는 것입니다. 이를 위한 코드는 매우 간단하지만 사용자 환경을 제대로 구현하기가 조금 더 어렵습니다.

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

e.clipboardData.files는 또 다른 FileList 객체입니다.

클립보드 API에서 까다로운 부분은 브라우저 간 완벽한 지원을 위해 타겟 요소를 선택하고 수정할 수 있어야 한다는 점입니다. contenteditable 속성이 있는 요소와 마찬가지로 <textarea><input type="text"> 모두 이 청구서에 해당합니다. 그러나 이는 텍스트 편집용으로 고안되었습니다.

사용자가 텍스트를 입력할 수 없도록 하려면 이 작업이 원활하게 작동하기 어려울 수 있습니다. 다른 요소를 클릭할 때 숨겨진 입력이 선택되는 등의 수법으로 인해 접근성을 유지하기가 더 어려워질 수 있습니다.

FileList 객체 처리

위의 메서드는 대부분 FileList를 생성하므로 이것이 무엇인지에 관해 간단히 설명하겠습니다.

FileListArray와 유사합니다. 숫자 키와 length 속성이 있지만 실제로 배열은 아닙니다. forEach() 또는 pop()와 같은 배열 메서드가 없으며 반복 가능하지 않습니다. 물론 Array.from(fileList)를 사용하여 실제 배열을 가져올 수 있습니다.

FileList의 항목은 File 객체입니다. namelastModified 읽기 전용 속성이 추가된다는 점을 제외하면 Blob 객체와 정확히 동일합니다.

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

이 예에서는 이미지 MIME 유형이 있는 첫 번째 파일을 찾지만 선택/붙여넣기/삭제되는 여러 이미지를 한 번에 처리할 수도 있습니다.

파일에 액세스하면 이 파일로 원하는 작업을 할 수 있습니다. 예를 들어 다음과 같이 변경할 수 있습니다.

  • 조작할 수 있도록 <canvas> 요소에 그리세요.
  • 사용자 기기에 다운로드
  • fetch()을 사용하여 서버에 업로드

대화형으로 카메라에 액세스

이제 기반을 다졌으니 점진적으로 개선할 차례입니다!

최신 브라우저에서는 카메라에 직접 액세스할 수 있으므로 웹페이지와 완전히 통합된 환경을 빌드할 수 있으므로 사용자가 브라우저를 나가지 않아도 됩니다.

카메라 액세스 권한 획득

WebRTC 사양의 getUserMedia()라는 API를 사용하여 카메라와 마이크에 직접 액세스할 수 있습니다. 이렇게 하면 사용자에게 연결된 마이크 및 카메라에 액세스하라는 메시지가 표시됩니다.

getUserMedia() 지원은 상당히 좋지만 아직 모든 곳에 있는 것은 아닙니다. 특히 Safari 10 이하에서는 사용할 수 없습니다. Safari는 이 문서 작성 시점에 여전히 최신 안정화 버전입니다. 그러나 Apple은 Safari 11에서 사용할 수 있다고 발표했습니다.

하지만 지원 여부를 파악하는 것은 매우 간단합니다.

const supported = 'mediaDevices' in navigator;

getUserMedia()를 호출할 때는 원하는 미디어의 종류를 설명하는 객체를 전달해야 합니다. 이러한 선택사항을 제약 조건이라고 합니다. 전면 카메라와 후면 카메라 중 선호하는 것, 오디오를 사용할지 여부, 스트림에 선호하는 해상도와 같은 여러 제약 조건이 있을 수 있습니다.

그러나 카메라에서 데이터를 가져오려면 제약 조건 하나만 있으면 됩니다. 바로 video: true입니다.

성공하면 API가 카메라의 데이터가 포함된 MediaStream를 반환하면 <video> 요소에 연결하여 실시간 미리보기를 표시하거나 <canvas>에 연결하여 스냅샷을 가져올 수 있습니다.

<video id="player" controls autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

그 자체로는 그다지 유용하지 않습니다. 동영상 데이터를 가져와 재생하기만 하면 됩니다. 이미지를 가져오려면 추가 작업을 해야 합니다.

스냅샷 가져오기

이미지를 가져오는 가장 좋은 방법은 동영상에서 캔버스로 프레임을 그리는 것입니다.

Web Audio API와 달리 웹 동영상 전용 스트림 처리 API가 없으므로 약간의 해킹 방법을 이용하여 사용자의 카메라에서 스냅샷을 캡처해야 합니다.

프로세스는 다음과 같습니다.

  1. 카메라에서 프레임을 고정할 캔버스 객체를 만듭니다.
  2. 카메라 스트림에 액세스
  3. 동영상 요소에 첨부
  4. 정확한 프레임을 캡처하려면 drawImage()를 사용하여 동영상 요소의 데이터를 캔버스 객체에 추가합니다.
<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

캔버스에 카메라의 데이터를 저장한 후 이 데이터로 많은 작업을 할 수 있습니다. 다음과 같은 방법을 사용할 수 있습니다.

  • 서버에 바로 업로드
  • 로컬에 저장
  • 이미지에 펑키한 효과 적용

필요하지 않은 경우 카메라에서 스트리밍 중지

더 이상 필요하지 않은 카메라는 사용을 중지하는 것이 좋습니다. 이렇게 하면 배터리와 처리 성능이 절약될 뿐만 아니라 사용자에게 애플리케이션에 대한 확신을 줍니다.

카메라 액세스를 중지하려면 getUserMedia()에서 반환한 스트림의 각 동영상 트랙에서 stop()를 호출하면 됩니다.

<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

책임감 있게 카메라를 사용하기 위한 권한 요청

사용자가 이전에 사이트에 카메라 액세스 권한을 부여하지 않은 경우 getUserMedia()를 호출하는 즉시 브라우저는 사용자에게 사이트에 카메라 액세스 권한을 부여하라는 메시지를 표시합니다.

사용자는 시스템에서 강력한 기기에 액세스하라는 메시지가 표시되는 것을 싫어하며, 메시지가 생성된 컨텍스트를 이해하지 못하면 요청을 자주 차단하거나 무시할 것입니다. 처음에 필요할 때만 카메라 액세스를 요청하는 것이 좋습니다. 사용자가 액세스 권한을 부여한 후에는 다시 메시지가 표시되지 않습니다. 그러나 사용자가 액세스를 거부하면 사용자가 수동으로 카메라 권한 설정을 변경하지 않는 한 다시 액세스할 수 없습니다.

호환성

모바일 및 데스크톱 브라우저 구현에 대한 추가 정보:

또한 adapter.js shim을 사용하여 WebRTC 사양 변경 및 프리픽스 차이로부터 앱을 보호하는 것이 좋습니다.

의견