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

Hầu hết các 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 dữ liệu đầu vào video và âm thanh từ người dùng. Tuy nhiên, tuỳ thuộc vào trình duyệt, đây có thể là trải nghiệm linh động và cùng dòng đầy đủ 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 ra trải nghiệm sử dụng hình ảnh do người dùng tạo và hoạt động hiệu quả ở mọi nơi?

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

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

Yêu cầu URL

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

Tuy nhiên, nếu bạn muốn thao tác với 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 các pixel thực tế, trừ phi máy chủ đặt các tiêu đề thích hợp và bạn đánh dấu hình ảnh là crossorigin; cách duy nhất thực tế để giải quyết vấn đề này là chạy máy chủ proxy.

Mục 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 một bộ lọc accept cho biết bạn chỉ muốn cá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, ứng dụng 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 chọn ứng dụng để chụp ảnh, bao gồm cả lựa chọn chụp ảnh trực tiếp bằng máy ảnh hoặc chọn một tệp hình ảnh hiện có.

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

Sau đó, bạn có thể đính kèm dữ liệu vào <form> hoặc thao tác bằng JavaScript bằng cách theo dõi sự kiện onchange trên phần tử đầ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à một đối tượng FileList. Tôi sẽ nói thêm về đối tượng này sau.

Bạn cũng có thể tuỳ ý thêm thuộc tính capture vào phần tử để 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 mà không có giá trị, thì trình duyệt sẽ quyết định dùng camera nào, trong khi các giá trị "user""environment" cho trình duyệt lần lượt ưu tiên camera trước và camera sau.

Thuộc tính capture hoạt động trên Android và iOS nhưng bị bỏ qua trên máy tính. Tuy nhiên, hãy lưu ý rằng trên Android, điều này có nghĩa là người dùng sẽ không còn lựa chọn chọn một bức ảnh hiện có nữa. Thay vào đó, ứng dụng máy ảnh 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ể sử 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.

Cách thứ nhất là thêm mục tiêu thả vào trang để cho phép người dùng 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ư dữ liệu đầu 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 về những 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 từ lâu 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 để lấy tệp hình ảnh hiện có là từ bảng nhớ tạm. Mã cho việc này rất đơn giản, nhưng trải nghiệm người dùng hơi khó 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 là một đối tượng FileList khác.)

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

Bạn có thể gặp khó khăn trong việc thực hiện thao tác này một cách trơn tru nếu không muốn người dùng nhập văn bản. Các thủ thuật như có một phương thức nhập ẩn được chọn khi bạn nhấp vào một số phần tử khác có thể làm cho việc duy trì khả năng hỗ trợ 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 nên nói một chút về nội dung này.

FileList tương tự như Array. Lớp này có các khoá số và thuộc tính length, nhưng thực ra không phải 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ể lấy một Mảng thực bằng cách sử dụng Array.from(fileList).

Các mục của FileList là đối tượng File. Các đối tượng này giống hệt với đối tượng Blob ngoại trừ việc chúng có thêm các thuộc tính chỉ có thể đọc 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 là hình ảnh, nhưng cũng có thể xử lý nhiều hình ảnh được chọn/dán/thả cùng một lúc.

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

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

Truy cập vào máy ảnh một cách tương tác

Giờ thì bạn đã nắm được những kiến thức cơ bản, đã đến lúc nâng cao 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 xây dựng các 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.

Lấy quyền 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 sử dụng một 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 cấp quyền truy cập vào micrô và máy ảnh đã kết nối.

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

Tuy nhiên, bạn có thể dễ dàng phát hiện tính năng hỗ trợ.

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 mà 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ố điều kiện ràng buộc có thể xảy ra, bao gồm cả việc bạn muốn dùng camera trước hay sau, bạn có muốn âm thanh hay không và độ phân giải bạn muốn cho luồng.

Tuy nhiên, để lấy dữ liệu từ máy ảnh, bạn chỉ cần một điều kiện ràng buộc duy nhất là video: true.

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

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

  const constraints = {
    video: true,
  };

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

Chỉ riêng thông tin này thì 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 tải hình ảnh xuống, bạn phải thực hiện thêm một số thao tác.

Chụp ảnh nhanh

Cách tốt nhất được hỗ trợ để lấy hình ảnh là vẽ một khung hình từ video lên canvas.

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

Quy trình này như sau:

  1. Tạo một đối tượng canvas sẽ chứa khung hình từ máy ảnh
  2. Truy cập vào luồng máy ảnh
  3. Đính kèm tệp đó vào một phần tử 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 playsinline 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 trữ dữ liệu từ máy ảnh trong canvas, bạn có thể làm nhiều việc với dữ liệu đó. Bạn có thể:

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

Mẹo

Dừng truyền trực tuyến từ máy ảnh khi không cần thiết

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

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

<video id="player" controls playsinline 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 cấp quyền sử dụng máy ảnh một cách có trách nhiệm

Nếu trước đó 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 lập tức khi bạn gọi getUserMedia(), trình duyệt sẽ nhắc người dùng cấp cho trang web của bạn quyền truy cập vào máy ảnh.

Người dùng không thích việc phải nhận lời nhắc truy cập vào các thiết bị hữu ích trên máy của họ và họ sẽ thường xuyên 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. Phương pháp hay nhất là chỉ yêu cầu quyền truy cập vào máy ảnh khi cần lần đầu. Sau khi người dùng cấp quyền truy cập, họ sẽ không được hỏi lại. Tuy nhiên, nếu người dùng từ chối cấp quyền truy cập, bạn sẽ không thể truy cập lại 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 dành cho 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 những thay đổi về thông số kỹ thuật và sự khác biệt về tiền tố WebRTC.

Phản hồi