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 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. 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. 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.
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 đó. Để chỉ hiển thị hình ảnh, cách này 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.
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 tệp hình ảnh hiện có.
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ử này để cho trình duyệt biết rằng bạn muốn lấy 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" />
Việc thêm thuộc tính capture
mà không có giá trị sẽ cho phép trình duyệt quyết định sử dụng máy ảnh nào, trong khi giá trị "user"
và "environment"
sẽ cho trình duyệt biết ưu tiên máy ảnh trước và sau tương ứng.
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ó. 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 bạn đang thêm tính năng tải tệp lên, thì có một số cách dễ dàng để bạn có thể 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 sẽ 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>
và <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 phím 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 khi 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 mụ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 một 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 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ể 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 các đối tượng này có thêm các thuộc tính chỉ có thể đọc name
và lastModified
.
<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 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 theo 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, nhờ đó 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ố quy tắc ràng buộc có thể áp dụng, bao gồm cả việc bạn muốn dùng máy ảnh mặt trước hay mặt 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 từ máy ảnh. Sau đó, bạn có thể đính kèm tệp này vào phần tử <video>
và phát để hiển thị bản xem trước theo thời gian thực hoặc đính kèm tệp này vào <canvas>
để lấy ảnh chụp 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 lấy hình ảnh, bạn phải làm thêm một chút.
Chụp ảnh nhanh
Cách tốt nhất để lấy hình ảnh được hỗ trợ là vẽ một khung hình từ video lên canvas.
Không giống như API Web Audio, không có API xử lý luồng chuyên dụng cho video trên web, vì vậy, bạn phải sử dụng một chút thủ thuật để chụp ảnh nhanh từ máy ảnh của người dùng.
Quy trình này như sau:
- Tạo một đối tượng canvas sẽ chứa khung hình từ máy ảnh
- Truy cập vào luồng máy ảnh
- Đính kèm tệp đó vào một phần tử video
- 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 trực tiếp lên máy chủ
- Lưu trữ cục bộ
- Áp dụng hiệu ứng thú vị 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 đây người dùng chưa cấp quyền truy cập vào máy ảnh cho trang web của bạn, 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 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ọ. Họ thường 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 ra lời nhắc. Tốt nhất là chỉ yêu cầu quyền truy cập vào máy ảnh khi cần thiết 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 khác 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 trình bổ trợ adapter.js để bảo vệ ứng dụng khỏi các thay đổi về thông số kỹ thuật WebRTC và sự khác biệt về tiền tố.