Ghi video từ người dùng

Mat Scales

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 của 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.

Việc dễ dàng nhất bạn có thể làm là yêu cầu người dùng cung cấp tệp đã ghi sẵn. Hãy thực hiện việc này bằng cách tạo một phần tử đầu vào tệp đơn giản và thêm bộ lọc accept cho biết chúng ta chỉ có thể chấp nhận tệp video và thuộc tính capture cho biết chúng ta muốn lấy tệp đó trực tiếp từ máy ảnh.

<input type="file" accept="video/*" capture />

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 một tệp lên từ hệ thống tệp (bỏ qua thuộc tính capture). Trong Safari trên iOS, thao tác này sẽ mở ứng dụng máy ảnh, cho phép bạn quay video rồi gửi video đó trở lại trang web; trên Android, thao tác này sẽ cho phép người dùng chọn ứng dụng để quay video trước khi gửi video đó trở lại trang web.

Nhiều thiết bị di động có nhiều camera. Nếu có lựa chọn ưu tiên, bạn có thể đặt thuộc tính capture thành user nếu muốn máy ảnh hướng về người dùng hoặc environment nếu muốn máy ảnh hướng ra ngoài.

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

Xin lưu ý rằng đây chỉ là gợi ý – nếu trình duyệt không hỗ trợ tuỳ chọn này hoặc không có loại máy ảnh bạn yêu cầu, thì trình duyệt có thể chọn một máy ảnh khác.

Sau khi người dùng hoàn tất quá trình ghi và quay lại trang web, bạn cần phải nắm bắt dữ liệu tệp bằng cách nào đó. Bạn có thể truy cập nhanh bằng cách đính kèm một sự kiện onchange vào phần tử đầu vào, sau đó đọc thuộc tính files của đối tượng sự kiện.

<input type="file" accept="video/*" capture="camera" id="recorder" />
<video id="player" controls></video>
<script>
  var recorder = document.getElementById('recorder');
  var player = document.getElementById('player');

  recorder.addEventListener('change', function (e) {
    var file = e.target.files[0];
    // Do something with the video file.
    player.src = URL.createObjectURL(file);
  });
</script>

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

  • Đính kèm trực tiếp vào phần tử <video> để bạn có thể phát
  • Tải xuống thiết bị của người dùng
  • Tải tệp lên máy chủ bằng cách đính kèm vào XMLHttpRequest
  • Vẽ các khung vào canvas và áp dụng bộ lọc cho canvas đó

Mặc dù phương thức phần tử đầu vào để truy cập dữ liệu video rất phổ biến, nhưng đây là phương thức ít hấp dẫn nhất. Chúng tôi thực sự muốn có quyền truy cập vào máy ảnh và mang đến trải nghiệm tốt ngay trên trang.

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

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

Lấy quyền truy cập vào máy ảnh

Chúng ta có thể truy cập trực tiếp vào máy ảnh bằng cách sử dụng một API trong thông số kỹ thuật WebRTC có tên là getUserMedia(). getUserMedia() 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.

Nếu thành công, API sẽ trả về một Stream chứa dữ liệu từ máy ảnh hoặc micrô. Sau đó, chúng ta có thể đính kèm dữ liệu đó vào một phần tử <video>, đính kèm vào luồng WebRTC hoặc lưu dữ liệu đó bằng API MediaRecorder.

Để lấy dữ liệu từ máy ảnh, chúng ta chỉ cần đặt video: true trong đối tượng ràng buộc được truyền đến API getUserMedia()

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

  var handleSuccess = function (stream) {
    player.srcObject = stream;
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: true})
    .then(handleSuccess);
</script>

Nếu muốn chọn một camera cụ thể, trước tiên, bạn có thể liệt kê các camera có sẵn.

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'videoinput');
});

Sau đó, bạn có thể truyền deviceId mà bạn muốn sử dụng khi gọi getUserMedia.

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    deviceId: devices[0].deviceId,
  },
});

Thực tế cho thấy thông tin này không thực sự hữu ích. Tất cả những gì chúng ta có thể làm là lấy dữ liệu video và phát lại.

Truy cập dữ liệu thô từ máy ảnh

Để truy cập vào dữ liệu video thô từ máy ảnh, bạn có thể vẽ từng khung hình vào một <canvas> và thao tác trực tiếp với các pixel.

Đối với canvas 2D, bạn có thể sử dụng phương thức drawImage của ngữ cảnh để vẽ khung hiện tại của phần tử <video> vào canvas.

context.drawImage(myVideoElement, 0, 0);

Với canvas WebGL, bạn có thể sử dụng phần tử <video> làm nguồn cho hoạ tiết.

gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  myVideoElement,
);

Lưu ý rằng trong cả hai trường hợp, quá trình này sẽ sử dụng khung hình hiện tại của video đang phát. Để xử lý nhiều khung hình, bạn cần vẽ lại video lên canvas mỗi lần.

Bạn có thể tìm hiểu thêm về vấn đề này trong bài viết của chúng tôi về cách áp dụng hiệu ứng theo thời gian thực cho hình ảnh và video.

Lưu dữ liệu từ máy ảnh

Cách dễ nhất để lưu dữ liệu từ máy ảnh là sử dụng API MediaRecorder.

API MediaRecorder sẽ lấy luồng do getUserMedia tạo, sau đó lưu dần dữ liệu từ luồng đó vào đích đến mà bạn muốn.

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  let shouldStop = false;
  let stopped = false;
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');

  stopButton.addEventListener('click', function() {
    shouldStop = true;
  })

  var handleSuccess = function(stream) {
    const options = {mimeType: 'video/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) {
        recordedChunks.push(e.data);
      }

      if(shouldStop === true && stopped === false) {
        mediaRecorder.stop();
        stopped = true;
      }
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.webm';
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: true })
      .then(handleSuccess);
</script>

Trong trường hợp này, chúng ta sẽ lưu dữ liệu trực tiếp vào một mảng mà sau này chúng ta có thể chuyển thành Blob. Sau đó, chúng ta có thể dùng Blob này để lưu vào Máy chủ web hoặc trực tiếp vào bộ nhớ trên thiết bị của người dùng.

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 đâ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 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 được nhắc cấp quyền truy cập vào các thiết bị mạnh trên máy của họ và họ thường xuyên chặn yêu cầu hoặc bỏ qua yêu cầu đó nếu không hiểu bối 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 họ từ chối quyền truy cập, bạn sẽ không thể truy cập lại để yêu cầu người dùng cấp quyền.

Sử dụng API quyền để kiểm tra xem bạn đã có quyền truy cập hay chưa

API getUserMedia không cho bạn biết liệu bạn đã có quyền truy cập vào máy ảnh hay chưa. Điều này sẽ gây ra vấn đề cho bạn. Để cung cấp một giao diện người dùng đẹp mắt giúp người dùng cấp cho bạn quyền truy cập vào máy ảnh, bạn phải yêu cầu quyền truy cập vào máy ảnh.

Bạn có thể giải quyết vấn đề này trong một số trình duyệt bằng cách sử dụng Permission API. API navigator.permission cho phép bạn truy vấn trạng thái của khả năng truy cập vào một API cụ thể mà không cần phải nhắc lại.

Để truy vấn xem bạn có quyền truy cập vào máy ảnh của người dùng hay không, bạn có thể truyền {name: 'camera'} vào phương thức truy vấn và phương thức này sẽ trả về:

  • granted — trước đây người dùng đã cấp cho bạn quyền truy cập vào máy ảnh;
  • prompt – người dùng chưa cấp cho bạn quyền truy cập và sẽ được nhắc khi bạn gọi getUserMedia;
  • denied – hệ thống hoặc người dùng đã chặn quyền truy cập vào camera một cách rõ ràng và bạn sẽ không thể truy cập vào camera đó.

Giờ đây, bạn có thể nhanh chóng kiểm tra xem có cần thay đổi giao diện người dùng để phù hợp với các hành động mà người dùng cần thực hiện hay không.

navigator.permissions.query({name: 'camera'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

Phản hồi