大多数浏览器都可以使用用户的摄像头。
许多浏览器现在都能访问用户的视频和音频输入。不过,根据浏览器的不同,这可能是完全动态的内嵌体验,也可能是委托给用户设备上的其他应用。最重要的是,并非所有设备都配有摄像头。那么,如何打造一种使用用户生成的图片的体验,使其在任何环境中都能顺畅使用?
从简单做起,循序渐进
如果您想逐步提升体验,需要先从适合所有环境的功能着手。最简单的方法是直接要求用户提供预先录制的文件。
询问网址
这是支持最好但最不令人满意的选项。让用户为您提供一个网址,然后使用该网址。如果只是显示图片,这在任何地方都适用。创建一个 img
元素,设置 src
就大功告成了。
不过,如果您想以任何方式操纵图片,情况就要复杂一些。CORS 会阻止您访问实际的像素,除非服务器设置了适当的标头并且您将图片标记为跨域;唯一可行的方法是运行代理服务器。
文件输入
您还可以使用简单的文件输入元素,包括指示您只需要图片文件的 accept
过滤器。
<input type="file" accept="image/*" />
此方法适用于所有平台。在桌面设备上,它会提示用户从文件系统上传图片文件。在 iOS 和 Android 版 Chrome 和 Safari 中,此方法可让用户选择使用哪个应用来拍摄图片,包括直接使用相机拍照或选择现有图片文件。
然后,数据可以附加到 <form>
或使用 JavaScript 进行操作,具体方法是监听输入元素上的 onchange
事件,然后读取事件 target
的 files
属性。
<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
,因此我应该简单说明一下它是什么。
FileList
与 Array
类似。它具有数字键和 length
属性,但实际上它不是数组。没有 forEach()
或 pop()
等数组方法,并且它不可迭代。当然,您可以使用 Array.from(fileList)
获取实际数组。
FileList
的条目是 File
对象。这些对象与 Blob
对象完全相同,只不过它们具有额外的 name
和 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>
此示例可查找具有图片 MIME 类型的第一个文件,但它也可以同时处理多张图片。
获得该文件的访问权限后,您便可对其执行任何所需的操作。例如,您可以:
- 将其绘制到
<canvas>
元素中,以便操纵它 - 将其下载到用户的设备
- 使用
fetch()
将其上传到服务器
以交互方式使用相机
现在您已经完成了基地的训练,是时候开始进步了!
现代浏览器可以直接使用摄像头,让您能够打造与网页完全集成的体验,让用户永远无需离开浏览器。
获取摄像头使用权限
您可以通过使用 WebRTC 规范中名为 getUserMedia()
的 API 来直接访问摄像头和麦克风。系统将提示用户授予对其连接的麦克风和摄像头的访问权限。
对 getUserMedia()
的支持非常好,但目前还没有全面支持。特别是,它在 Safari 10 或更低版本中不可用,因为在撰写本文时,Safari 10 仍是最新的稳定版。不过,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 不同,Web 视频没有专用的流处理 API,因此您必须利用一些技巧才能从用户的摄像头捕获快照。
具体过程如下:
- 创建一个画布对象,用于容纳相机中的画面
- 获取对摄像头画面的访问权限
- 附加到视频元素
- 如果要精确捕获帧,请使用
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 规范更改和前缀差异的影响。