网页调用的是用户电脑或手机上的摄像头,不是网络相机的 RTSP 地址。getUserMedia 需要安全上下文:本机 localhost 可以实验,跨机器部署使用 HTTPS。
保存一个拍照页面
保存为 camera.html,在文件目录运行 python -m http.server 8092 --bind 127.0.0.1,然后打开 http://127.0.0.1:8092/camera.html:
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>摄像头拍照</title><body> <button id="open">打开摄像头</button> <button id="capture" disabled>拍照</button> <button id="close">关闭</button> <p id="status"></p> <video id="video" autoplay muted playsinline style="max-width:100%"></video> <canvas id="canvas" hidden></canvas> <img id="preview" alt="拍照预览" style="max-width:100%"> <a id="download" hidden download="photo.jpg">下载照片</a> <script> const video = document.querySelector('#video') const capture = document.querySelector('#capture') const status = document.querySelector('#status') const preview = document.querySelector('#preview') const download = document.querySelector('#download') let stream, photoUrl, version = 0, capturing = false function close() { version++ stream?.getTracks().forEach(track => track.stop()) stream = undefined video.srcObject = null capture.disabled = true } document.querySelector('#open').onclick = async () => { close() const current = version try { if (!navigator.mediaDevices?.getUserMedia) throw new Error('需要 HTTPS 或 localhost') const next = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) if (current !== version) { next.getTracks().forEach(track => track.stop()) return } stream = next video.srcObject = stream await video.play() if (current === version) status.textContent = '摄像头已打开,等待画面' } catch (error) { if (current !== version) return close() status.textContent = `打开失败:${error.name || error.message}` } } video.onloadeddata = () => { if (stream && video.videoWidth > 0) capture.disabled = false } document.querySelector('#close').onclick = () => { close() status.textContent = '摄像头已关闭' } capture.onclick = async () => { if (capturing || !stream || !video.videoWidth) return capturing = true try { const canvas = document.querySelector('#canvas') canvas.width = video.videoWidth canvas.height = video.videoHeight canvas.getContext('2d').drawImage(video, 0, 0) const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)) if (!blob) throw new Error('图片编码失败') if (photoUrl) URL.revokeObjectURL(photoUrl) photoUrl = URL.createObjectURL(blob) preview.src = photoUrl download.href = photoUrl download.hidden = false status.textContent = '照片已生成,点击下载' } catch (error) { status.textContent = error.message } finally { capturing = false } } document.addEventListener('visibilitychange', () => { if (document.hidden) close() }) window.addEventListener('pagehide', () => { close() if (photoUrl) URL.revokeObjectURL(photoUrl) }) </script></body></html>videoWidth/videoHeight 是实际帧尺寸,不能用页面 CSS 宽高替代。拍照结果是 Blob,生成临时对象 URL 用于预览,换照片和离开页面时释放。这里的“下载”没有上传服务器,也不能证明用户已完成保存。
打开后再关闭,设备指示灯应熄灭;权限请求晚返回时也会停止那条流。拒绝权限、没有设备或设备被占用时,显示错误并由用户重新操作,不递归无限切换摄像头。
评论