Webcam capture for photo ID via getUserMedia

Desktop users can now use their webcam to photograph their ID:
- Click "Use Camera" → browser requests webcam permission
- Live video preview with orange guide rectangle for ID placement
- Capture button takes high-res JPEG (1280x720)
- Cancel button stops webcam and returns to upload options
- Captured image goes through same quality check flow
- Works on Chrome, Firefox, Edge, Safari (desktop + mobile)
- No libraries needed — native WebRTC API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-30 16:09:02 -05:00
parent c40dfb552e
commit 39fb1c9998

View file

@ -238,12 +238,24 @@
<svg style="width:20px;height:20px;color:#64748b" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>
Upload File
</button>
<button type="button" id="dot-id-scan-btn" style="display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;color:#374151;font-size:13px;font-weight:500">
<button type="button" id="dot-id-cam-btn" style="display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;color:#374151;font-size:13px;font-weight:500">
<svg style="width:20px;height:20px;color:#64748b" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/></svg>
Camera / Scanner
Use Camera
</button>
</div>
<input type="file" id="dot-photo-id-scan" accept="image/*" capture="environment" style="display:none" />
<!-- Webcam capture area (hidden until activated) -->
<div id="dot-id-webcam" hidden style="margin-top:12px">
<div style="position:relative;max-width:400px;margin:0 auto;border-radius:8px;overflow:hidden;border:2px solid #1a2744">
<video id="dot-id-video" autoplay playsinline style="width:100%;display:block;background:#000"></video>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;border:2px dashed rgba(249,115,22,0.6);border-radius:8px;width:85%;height:60%"></div>
</div>
<p style="font-size:11px;color:#64748b;text-align:center;margin:6px 0">Position your ID within the orange guide</p>
<div style="display:flex;gap:8px;justify-content:center;margin-top:8px">
<button type="button" id="dot-id-capture" style="padding:10px 32px;background:#f97316;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer">&#128247; Capture</button>
<button type="button" id="dot-id-cam-cancel" style="padding:10px 20px;background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:8px;font-size:13px;cursor:pointer">Cancel</button>
</div>
</div>
<canvas id="dot-id-canvas" style="display:none"></canvas>
<div style="text-align:center;margin:12px 0 0">
<div style="display:flex;align-items:center;gap:12px;justify-content:center;margin-bottom:8px">
<div style="flex:1;height:1px;background:#d1d5db"></div>
@ -451,10 +463,66 @@
// Upload button — opens file picker
idBtn?.addEventListener("click", function() { if (idInput) idInput.click(); });
// Scanner/camera button
var idScanBtn = document.getElementById("dot-id-scan-btn");
var idScanInput = document.getElementById("dot-photo-id-scan");
idScanBtn?.addEventListener("click", function() { if (idScanInput) idScanInput.click(); });
// Webcam camera button
var idCamBtn = document.getElementById("dot-id-cam-btn");
var idWebcam = document.getElementById("dot-id-webcam");
var idVideo = document.getElementById("dot-id-video");
var idCaptureBtn = document.getElementById("dot-id-capture");
var idCamCancel = document.getElementById("dot-id-cam-cancel");
var idCanvas = document.getElementById("dot-id-canvas");
var webcamStream = null;
idCamBtn?.addEventListener("click", function() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert("Camera not available in this browser. Please use Upload File instead.");
return;
}
navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: "environment" } })
.then(function(stream) {
webcamStream = stream;
idVideo.srcObject = stream;
if (idWebcam) idWebcam.hidden = false;
if (idUploadOpts) {
// Hide upload options but keep webcam visible
var buttons = idUploadOpts.querySelector("div");
if (buttons) buttons.style.display = "none";
}
})
.catch(function(err) {
alert("Could not access camera: " + err.message + ". Please use Upload File instead.");
});
});
idCaptureBtn?.addEventListener("click", function() {
if (!idVideo || !idCanvas) return;
idCanvas.width = idVideo.videoWidth;
idCanvas.height = idVideo.videoHeight;
var ctx = idCanvas.getContext("2d");
ctx.drawImage(idVideo, 0, 0);
// Stop webcam
if (webcamStream) {
webcamStream.getTracks().forEach(function(t) { t.stop(); });
webcamStream = null;
}
if (idWebcam) idWebcam.hidden = true;
// Convert to blob and handle as file
idCanvas.toBlob(function(blob) {
var file = new File([blob], "photo-id-capture.jpg", { type: "image/jpeg" });
handleIdFile(file);
}, "image/jpeg", 0.92);
});
idCamCancel?.addEventListener("click", function() {
if (webcamStream) {
webcamStream.getTracks().forEach(function(t) { t.stop(); });
webcamStream = null;
}
if (idWebcam) idWebcam.hidden = true;
if (idUploadOpts) {
var buttons = idUploadOpts.querySelector("div");
if (buttons) buttons.style.display = "";
}
});
// QR code button — show QR with current page URL for phone upload
idQrBtn?.addEventListener("click", function() {
@ -597,7 +665,9 @@
window.__dotPhotoId = null;
idAccepted = false;
if (idInput) idInput.value = "";
if (idScanInput) idScanInput.value = "";
// Stop webcam if running
if (webcamStream) { webcamStream.getTracks().forEach(function(t) { t.stop(); }); webcamStream = null; }
if (idWebcam) idWebcam.hidden = true;
if (idPreview) idPreview.hidden = true;
if (idUploadOpts) idUploadOpts.style.display = "";
// Reset quality check UI
@ -612,10 +682,7 @@
handleIdFile(idInput.files?.[0]);
});
// Scanner input handler
idScanInput?.addEventListener("change", function() {
handleIdFile(idScanInput.files?.[0]);
});
// (webcam capture handled above via getUserMedia)
} // end guard
</script>