Spaces:
Running
Running
LogicalTrue commited on
Commit ·
cea73c0
1
Parent(s): 9adc2d0
feat: add live WebRTC camera stream viewfinder modal with capture trigger for mobile
Browse files- frontend/viewer.html +75 -4
frontend/viewer.html
CHANGED
|
@@ -578,6 +578,25 @@
|
|
| 578 |
</div>
|
| 579 |
</form>
|
| 580 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
</div>
|
| 582 |
|
| 583 |
<!-- Privacy & Security Modal (Mobile) -->
|
|
@@ -1387,15 +1406,67 @@
|
|
| 1387 |
});
|
| 1388 |
}
|
| 1389 |
|
| 1390 |
-
// Upload & Camera handlers
|
| 1391 |
const cameraInputMobile = document.getElementById('cameraInputMobile');
|
| 1392 |
const btnTriggerCameraMobile = document.getElementById('btnTriggerCameraMobile');
|
| 1393 |
const btnTriggerFileMobile = document.getElementById('btnTriggerFileMobile');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1394 |
|
| 1395 |
-
if (btnTriggerCameraMobile
|
| 1396 |
-
btnTriggerCameraMobile.addEventListener('click', (e) => {
|
| 1397 |
e.stopPropagation();
|
| 1398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1399 |
});
|
| 1400 |
}
|
| 1401 |
|
|
|
|
| 578 |
</div>
|
| 579 |
</form>
|
| 580 |
</div>
|
| 581 |
+
<!-- Native Live Camera Stream Modal (Mobile WebRTC) -->
|
| 582 |
+
<div id="liveCameraModal" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: #000000; flex-direction: column; align-items: center; justify-content: space-between; padding: 16px; box-sizing: border-box;">
|
| 583 |
+
<div style="width: 100%; max-width: 600px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px;">
|
| 584 |
+
<div style="font-weight: 700; font-size: 1rem; color: #fff; display: flex; align-items: center; gap: 8px;">
|
| 585 |
+
📸 Tomar Foto
|
| 586 |
+
</div>
|
| 587 |
+
<button id="btnCloseLiveCamera" style="background: rgba(255,255,255,0.2); border: none; color: #fff; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1rem; display: flex; align-items: center; justify-content: center;">✕</button>
|
| 588 |
+
</div>
|
| 589 |
+
|
| 590 |
+
<div style="width: 100%; max-width: 600px; height: calc(100vh - 160px); display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; background: #121214; border-radius: 16px;">
|
| 591 |
+
<video id="liveCameraVideo" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover; border-radius: 16px;"></video>
|
| 592 |
+
<canvas id="liveCameraCanvas" style="display: none;"></canvas>
|
| 593 |
+
</div>
|
| 594 |
+
|
| 595 |
+
<div style="width: 100%; max-width: 600px; display: flex; justify-content: center; padding: 10px 0;">
|
| 596 |
+
<button type="button" id="btnCaptureLivePhoto" style="background: #ffffff; color: #121214; border: 4px solid #7c3aed; width: 70px; height: 70px; border-radius: 50%; font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(124, 58, 237, 0.6); transition: transform 0.1s active;">
|
| 597 |
+
📸
|
| 598 |
+
</button>
|
| 599 |
+
</div>
|
| 600 |
</div>
|
| 601 |
|
| 602 |
<!-- Privacy & Security Modal (Mobile) -->
|
|
|
|
| 1406 |
});
|
| 1407 |
}
|
| 1408 |
|
| 1409 |
+
// Upload & Live Camera WebRTC handlers
|
| 1410 |
const cameraInputMobile = document.getElementById('cameraInputMobile');
|
| 1411 |
const btnTriggerCameraMobile = document.getElementById('btnTriggerCameraMobile');
|
| 1412 |
const btnTriggerFileMobile = document.getElementById('btnTriggerFileMobile');
|
| 1413 |
+
const liveCameraModal = document.getElementById('liveCameraModal');
|
| 1414 |
+
const liveCameraVideo = document.getElementById('liveCameraVideo');
|
| 1415 |
+
const liveCameraCanvas = document.getElementById('liveCameraCanvas');
|
| 1416 |
+
const btnCloseLiveCamera = document.getElementById('btnCloseLiveCamera');
|
| 1417 |
+
const btnCaptureLivePhoto = document.getElementById('btnCaptureLivePhoto');
|
| 1418 |
+
let cameraStream = null;
|
| 1419 |
+
|
| 1420 |
+
function stopCameraStream() {
|
| 1421 |
+
if (cameraStream) {
|
| 1422 |
+
cameraStream.getTracks().forEach(track => track.stop());
|
| 1423 |
+
cameraStream = null;
|
| 1424 |
+
}
|
| 1425 |
+
if (liveCameraModal) liveCameraModal.style.display = 'none';
|
| 1426 |
+
}
|
| 1427 |
|
| 1428 |
+
if (btnTriggerCameraMobile) {
|
| 1429 |
+
btnTriggerCameraMobile.addEventListener('click', async (e) => {
|
| 1430 |
e.stopPropagation();
|
| 1431 |
+
// Intentar abrir cámara en vivo mediante MediaDevices WebRTC
|
| 1432 |
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
| 1433 |
+
try {
|
| 1434 |
+
cameraStream = await navigator.mediaDevices.getUserMedia({
|
| 1435 |
+
video: { facingMode: { ideal: 'environment' } },
|
| 1436 |
+
audio: false
|
| 1437 |
+
});
|
| 1438 |
+
liveCameraVideo.srcObject = cameraStream;
|
| 1439 |
+
liveCameraModal.style.display = 'flex';
|
| 1440 |
+
return;
|
| 1441 |
+
} catch (err) {
|
| 1442 |
+
console.warn("[Camera WebRTC Fallback] Direct camera access fallback to input capture:", err);
|
| 1443 |
+
}
|
| 1444 |
+
}
|
| 1445 |
+
// Fallback si no hay soporte WebRTC directo o permiso denegado: disparar input capture
|
| 1446 |
+
if (cameraInputMobile) cameraInputMobile.click();
|
| 1447 |
+
});
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
if (btnCloseLiveCamera) {
|
| 1451 |
+
btnCloseLiveCamera.addEventListener('click', stopCameraStream);
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
if (btnCaptureLivePhoto && liveCameraVideo && liveCameraCanvas) {
|
| 1455 |
+
btnCaptureLivePhoto.addEventListener('click', () => {
|
| 1456 |
+
const width = liveCameraVideo.videoWidth || 1024;
|
| 1457 |
+
const height = liveCameraVideo.videoHeight || 1024;
|
| 1458 |
+
liveCameraCanvas.width = width;
|
| 1459 |
+
liveCameraCanvas.height = height;
|
| 1460 |
+
const ctx = liveCameraCanvas.getContext('2d');
|
| 1461 |
+
ctx.drawImage(liveCameraVideo, 0, 0, width, height);
|
| 1462 |
+
|
| 1463 |
+
base64Image = liveCameraCanvas.toDataURL('image/png');
|
| 1464 |
+
imagePreviewMobile.src = base64Image;
|
| 1465 |
+
dropzone.style.display = 'none';
|
| 1466 |
+
imagePreviewContainer.style.display = 'block';
|
| 1467 |
+
btnCreate3DMobile.style.display = 'flex';
|
| 1468 |
+
|
| 1469 |
+
stopCameraStream();
|
| 1470 |
});
|
| 1471 |
}
|
| 1472 |
|