Chụp ảnh từ người dùng

Hầu hết trình duyệt đều có thể truy cập vào máy ảnh của người dùng.

Nhiều trình duyệt hiện có khả năng truy cập vào video và âm thanh đầu vào từ người dùng. Tuy nhiên, tuỳ thuộc vào trình duyệt, đó có thể là một trải nghiệm động và cùng dòng hoàn toàn hoặc có thể được uỷ quyền cho một ứng dụng khác trên thiết bị của người dùng. Ngoài ra, không phải thiết bị nào cũng có máy ảnh. Vậy làm cách nào để bạn có thể tạo trải nghiệm sử dụng hình ảnh do người dùng tạo hoạt động tốt ở mọi nơi?

Bắt đầu đơn giản và tăng dần

Nếu muốn cải thiện dần trải nghiệm của mình, bạn cần bắt đầu bằng một phương pháp hiệu quả ở mọi nơi. Cách dễ nhất cần làm là yêu cầu người dùng cung cấp tệp được ghi sẵn.

Yêu cầu một URL

Đây là lựa chọn được hỗ trợ tốt nhất nhưng ít hài lòng nhất. Yêu cầu người dùng cung cấp URL cho bạn, sau đó sử dụng URL đó. Để chỉ hiển thị hình ảnh, thao tác này sẽ hoạt động ở mọi nơi. Tạo một phần tử img, đặt src và thế là bạn đã hoàn tất.

Tuy nhiên, nếu bạn muốn điều khiển hình ảnh theo bất kỳ cách nào, mọi thứ sẽ phức tạp hơn một chút. CORS ngăn bạn truy cập vào pixel thực tế trừ phi máy chủ đặt tiêu đề thích hợp và bạn đánh dấu hình ảnh là crossorigin; cách duy nhất để thực hiện việc này là chạy máy chủ proxy.

Nhập tệp

Bạn cũng có thể sử dụng một phần tử nhập tệp đơn giản, bao gồm cả bộ lọc accept cho biết bạn chỉ muốn có tệp hình ảnh.

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

Phương thức này hoạt động trên tất cả các nền tảng. Trên máy tính, thao tác này sẽ nhắc người dùng tải tệp hình ảnh lên từ hệ thống tệp. Trong Chrome và Safari trên iOS và Android, phương thức này sẽ cho phép người dùng lựa chọn ứng dụng sẽ chụp ảnh, bao gồm cả tuỳ chọn chụp ảnh trực tiếp bằng máy ảnh hoặc chọn tệp hình ảnh hiện có.

Trình đơn Android có 2 tuỳ chọn: chụp ảnh và tệp Trình đơn iOS có 3 tuỳ chọn: chụp ảnh, thư viện ảnh, iCloud

Sau đó, dữ liệu có thể được đính kèm vào <form> hoặc điều khiển bằng JavaScript bằng cách lắng nghe sự kiện onchange trên thành phần đầu vào, sau đó đọc thuộc tính files của sự kiện target.

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

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

Thuộc tính files là đối tượng FileList mà tôi sẽ nói thêm sau này.

Bạn cũng có thể tuỳ ý thêm thuộc tính capture vào phần tử để cho trình duyệt biết rằng bạn muốn nhận hình ảnh từ máy ảnh.

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

Nếu bạn thêm thuộc tính capture không có giá trị, trình duyệt sẽ quyết định máy ảnh sẽ sử dụng, còn giá trị "user""environment" cho trình duyệt ưu tiên máy ảnh trước và máy ảnh sau tương ứng.

Thuộc tính capture có trên Android và iOS, nhưng sẽ bị bỏ qua trên máy tính. Tuy nhiên, hãy lưu ý rằng điều này có nghĩa là người dùng sẽ không thể chọn một hình ảnh hiện có trên Android nữa. Thay vào đó, ứng dụng máy ảnh của hệ thống sẽ được khởi động trực tiếp.

Kéo và thả

Nếu đã thêm khả năng tải tệp lên, bạn có thể áp dụng một số cách đơn giản để làm cho trải nghiệm người dùng phong phú hơn một chút.

Đầu tiên là thêm đích thả vào trang của bạn để người dùng có thể kéo một tệp từ máy tính hoặc một ứng dụng khác.

<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>

Tương tự như giá trị nhập vào tệp, bạn có thể lấy đối tượng FileList từ thuộc tính dataTransfer.files của sự kiện drop;

Trình xử lý sự kiện dragover cho phép bạn báo hiệu cho người dùng điều gì sẽ xảy ra khi họ thả tệp bằng cách sử dụng thuộc tính dropEffect.

Tính năng kéo và thả đã xuất hiện trong một thời gian dài và được các trình duyệt chính hỗ trợ tốt.

Dán từ bảng nhớ tạm

Cách cuối cùng để tải tệp hình ảnh hiện có là từ bảng nhớ tạm. Mã cho thao tác này rất đơn giản, nhưng trải nghiệm người dùng lại khó chính xác hơn một chút.

<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 vẫn là một đối tượng FileList khác.)

Một khó khăn với API bảng nhớ tạm là để hỗ trợ đầy đủ nhiều trình duyệt, phần tử mục tiêu cần phải vừa dễ chọn vừa chỉnh sửa được. Cả <textarea><input type="text"> đều phù hợp với hoá đơn ở đây, cũng như các phần tử có thuộc tính contenteditable. Nhưng các trình xử lý này cũng được thiết kế rõ ràng để chỉnh sửa văn bản.

Nếu không muốn người dùng có thể nhập văn bản, thì thật khó có thể khiến công việc này diễn ra suôn sẻ. Các thủ thuật như chọn một dữ liệu đầu vào ẩn khi bạn nhấp vào một số phần tử khác có thể khiến việc duy trì khả năng tiếp cận trở nên khó khăn hơn.

Xử lý đối tượng FileList

Vì hầu hết các phương thức ở trên đều tạo ra FileList, nên tôi sẽ nói một chút về khái niệm đó.

FileList tương tự như Array. Lớp này có các khoá số và thuộc tính length, nhưng không thực sự là một mảng. Không có phương thức mảng nào, như forEach() hoặc pop() và không thể lặp lại. Tất nhiên, bạn có thể nhận một Array thực bằng cách sử dụng Array.from(fileList).

Các mục nhập của FileList là các đối tượng File. Các đối tượng này giống hệt như đối tượng Blob, ngoại trừ việc có thêm các thuộc tính chỉ có thể đọc là namelastModified.

<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>

Ví dụ này tìm tệp đầu tiên có loại MIME hình ảnh, nhưng cũng có thể xử lý nhiều hình ảnh được chọn/dán/thả cùng lúc.

Sau khi có quyền truy cập vào tệp, bạn có thể làm bất cứ việc gì bạn muốn với tệp đó. Ví dụ: bạn có thể:

  • Vẽ vào phần tử <canvas> để bạn có thể thao tác
  • Tải xuống thiết bị của người dùng
  • Tải lên máy chủ bằng fetch()

Truy cập máy ảnh theo cách tương tác

Giờ đây, bạn đã nắm bắt được thông tin cơ bản, đã đến lúc nâng cấp dần!

Các trình duyệt hiện đại có thể truy cập trực tiếp vào máy ảnh, cho phép bạn tạo trải nghiệm được tích hợp đầy đủ với trang web, vì vậy, người dùng không bao giờ cần rời khỏi trình duyệt.

Truy cập vào máy ảnh

Bạn có thể truy cập trực tiếp vào máy ảnh và micrô bằng cách dùng API trong thông số kỹ thuật WebRTC có tên là getUserMedia(). Thao tác này sẽ nhắc người dùng truy cập vào micrô và máy ảnh đã kết nối.

Hỗ trợ dành cho getUserMedia() là khá tốt, nhưng vẫn chưa phải ở mọi nơi. Cụ thể, tính năng này không có trong Safari 10 trở xuống. Tại thời điểm viết bài viết này, phiên bản này vẫn là phiên bản ổn định mới nhất. Tuy nhiên, Apple đã thông báo rằng ứng dụng này sẽ có trong Safari 11.

Tuy nhiên, việc phát hiện hỗ trợ rất đơn giản.

const supported = 'mediaDevices' in navigator;

Khi gọi getUserMedia(), bạn cần truyền vào một đối tượng mô tả loại nội dung nghe nhìn bạn muốn. Những lựa chọn này được gọi là quy tắc ràng buộc. Có một số quy tắc hạn chế có thể có, chẳng hạn như bạn thích máy ảnh mặt trước hay máy ảnh mặt sau, việc bạn muốn có âm thanh và độ phân giải ưu tiên của luồng phát.

Tuy nhiên, để nhận dữ liệu từ máy ảnh, bạn chỉ cần một quy tắc ràng buộc, đó là video: true.

Nếu thành công, API sẽ trả về một MediaStream chứa dữ liệu từ máy ảnh, sau đó bạn có thể đính kèm dữ liệu đó vào một phần tử <video> và phát phần tử đó để hiển thị bản xem trước theo thời gian thực hoặc đính kèm vào <canvas> để lấy ảnh chụp nhanh.

<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>

Bản thân nó thì điều này đã không hữu ích lắm. Tất cả những gì bạn có thể làm là lấy dữ liệu video và phát lại. Nếu muốn nhận hình ảnh, bạn phải làm thêm một chút.

Chụp ảnh nhanh

Lựa chọn phù hợp nhất để nhận hình ảnh là vẽ một khung từ video lên canvas.

Không giống như API Web âm thanh, không có API xử lý luồng chuyên dụng cho video trên web, vì vậy, bạn phải dùng một chút tin tặc để chụp ảnh nhanh từ máy ảnh của người dùng.

Quá trình này diễn ra như sau:

  1. Tạo một đối tượng canvas sẽ giữ khung từ máy ảnh
  2. Truy cập vào luồng camera
  3. Đính kèm video đó vào một thành phần video
  4. Khi bạn muốn chụp một khung hình chính xác, hãy thêm dữ liệu từ phần tử video vào đối tượng canvas bằng 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>

Sau khi đã lưu dữ liệu từ máy ảnh trong canvas, bạn có thể làm nhiều việc với máy ảnh. Bạn có thể:

  • Tải thẳng lên máy chủ
  • Lưu trữ cục bộ
  • Áp dụng các hiệu ứng sôi động cho hình ảnh

Mẹo

Ngừng xem trực tuyến từ camera khi không cần thiết

Bạn nên ngừng sử dụng camera khi không cần nữa. Việc này không chỉ giúp tiết kiệm pin và năng lượng xử lý mà còn giúp người dùng tin tưởng vào ứng dụng của bạn.

Để dừng quyền truy cập vào máy ảnh, bạn chỉ cần gọi stop() trên từng bản video cho luồng do getUserMedia() trả về.

<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>

Yêu cầu quyền sử dụng máy ảnh một cách có trách nhiệm

Nếu trước đây người dùng chưa cấp cho trang web của bạn quyền truy cập vào máy ảnh, thì ngay khi bạn gọi getUserMedia(), trình duyệt sẽ nhắc người dùng cấp quyền truy cập vào máy ảnh cho trang web của bạn.

Người dùng ghét việc phải nhận lời nhắc truy cập vào các thiết bị mạnh mẽ trên máy của họ. Họ thường chặn yêu cầu, hoặc sẽ bỏ qua nếu không hiểu ngữ cảnh tạo lời nhắc. Tốt nhất là bạn chỉ nên yêu cầu quyền truy cập vào máy ảnh khi cần. Sau khi cấp quyền truy cập, người dùng sẽ không được yêu cầu nữa. Tuy nhiên, nếu người dùng từ chối quyền truy cập thì bạn sẽ không thể lấy lại quyền truy cập, trừ phi họ thay đổi chế độ cài đặt quyền truy cập vào máy ảnh theo cách thủ công.

Khả năng tương thích

Thông tin thêm về cách triển khai trình duyệt trên thiết bị di động và máy tính:

Bạn cũng nên sử dụng miếng đệm adapter.js để bảo vệ các ứng dụng khỏi các thay đổi về thông số kỹ thuật của WebRTC và sự khác biệt về tiền tố.

Ý kiến phản hồi