822 lines
31 KiB
HTML
822 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Camera Text Recognition - API Integrated</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
color: white;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.capture-mode {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
gap: 20px;
|
|
padding-bottom: 200px;
|
|
}
|
|
|
|
.camera-container {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 600px;
|
|
background: black;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.8);
|
|
}
|
|
|
|
video {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
canvas {
|
|
display: none;
|
|
}
|
|
|
|
.crosshair {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 250px;
|
|
height: 250px;
|
|
border: 3px solid #06b6d4;
|
|
border-radius: 8px;
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.camera-hint {
|
|
position: absolute;
|
|
bottom: 15px;
|
|
left: 0;
|
|
right: 0;
|
|
text-align: center;
|
|
color: #06b6d4;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
gap: 16px;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.status-bar span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #ef4444;
|
|
}
|
|
|
|
.status-dot.ready {
|
|
background: #10b981;
|
|
}
|
|
|
|
.result-box {
|
|
background: rgba(15, 23, 42, 0.8);
|
|
border: 2px solid #06b6d4;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
text-align: center;
|
|
min-height: 120px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
|
|
.result-box label {
|
|
display: block;
|
|
color: #06b6d4;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.result-box p {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: white;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.result-box.error {
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
.result-box.error label {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.result-box.success {
|
|
border-color: #10b981;
|
|
}
|
|
|
|
.result-box.success label {
|
|
color: #10b981;
|
|
}
|
|
|
|
.result-box.not-found {
|
|
border-color: #f97316;
|
|
}
|
|
|
|
.result-box.not-found label {
|
|
color: #f97316;
|
|
}
|
|
|
|
.controls {
|
|
background: #1e293b;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: center;
|
|
max-width: 600px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 100;
|
|
width: calc(100% - 40px);
|
|
}
|
|
|
|
.language-select {
|
|
background: #334155;
|
|
border: 1px solid #475569;
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(135deg, #06b6d4 0%, #0284c7 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(6, 182, 212, 0.3);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #475569;
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.playing-mode {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: black;
|
|
position: relative;
|
|
}
|
|
|
|
.video-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
.video-player {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.video-title {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
z-index: 10;
|
|
}
|
|
|
|
.video-meta {
|
|
position: absolute;
|
|
top: 70px;
|
|
left: 20px;
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
z-index: 10;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.close-button {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #dc2626 !important;
|
|
padding: 12px 16px;
|
|
border-radius: 50%;
|
|
z-index: 10;
|
|
width: 48px;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.close-button:hover {
|
|
background: #991b1b !important;
|
|
}
|
|
|
|
.controls-bar {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: linear-gradient(to top, black, transparent);
|
|
padding: 40px 20px 20px;
|
|
display: flex;
|
|
gap: 16px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.spinner {
|
|
display: inline-block;
|
|
border: 3px solid #334155;
|
|
border-top: 3px solid #06b6d4;
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.confidence {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.error-message {
|
|
color: #ef4444;
|
|
font-size: 14px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.info-box {
|
|
position: absolute;
|
|
bottom: 100px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border: 1px solid #334155;
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
max-width: 200px;
|
|
}
|
|
|
|
.match-details {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
margin-top: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- CAPTURE MODE -->
|
|
<div id="captureMode" class="capture-mode">
|
|
<div class="status-bar">
|
|
<span><span class="status-dot" id="ocrDot"></span> OCR: <span id="ocrStatus">Loading...</span></span>
|
|
<span><span class="status-dot" id="apiDot"></span> API: <span id="apiStatus">Checking...</span></span>
|
|
</div>
|
|
|
|
<div class="camera-container">
|
|
<video id="video" autoplay playsinline></video>
|
|
<canvas id="canvas"></canvas>
|
|
<div class="crosshair"></div>
|
|
<div class="camera-hint">Point camera at text to recognize</div>
|
|
</div>
|
|
|
|
<div id="resultBox" class="result-box hidden">
|
|
<label id="resultLabel">Recognized Text:</label>
|
|
<p id="resultText"></p>
|
|
<p id="resultStatus"></p>
|
|
<div class="match-details" id="matchDetails"></div>
|
|
<div class="confidence" id="confidence"></div>
|
|
<div class="error-message" id="errorMessage"></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<label for="language">Language:</label>
|
|
<select id="language" class="language-select">
|
|
<option value="en">English</option>
|
|
<option value="fr">Français</option>
|
|
<option value="de">Deutsch</option>
|
|
<option value="es">Español</option>
|
|
<option value="it">Italiano</option>
|
|
</select>
|
|
<button id="permissionBtn" onclick="initCamera()" style="background: #3b82f6;">
|
|
🔐 Request Camera Permission
|
|
</button>
|
|
<button id="captureBtn" onclick="captureAndRecognize()" disabled>
|
|
📷 Capture & Recognize
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PLAYING MODE -->
|
|
<div id="playingMode" class="playing-mode hidden">
|
|
<div class="video-wrapper">
|
|
<div class="video-title" id="videoTitle"></div>
|
|
<div class="video-meta" id="videoMeta"></div>
|
|
<button class="close-button" onclick="closeVideo()">✕</button>
|
|
<video id="videoPlayer" class="video-player" autoplay muted controls></video>
|
|
<div class="info-box" id="infoBox"></div>
|
|
</div>
|
|
|
|
<div class="controls-bar">
|
|
<button id="playPauseBtn" onclick="togglePlayPause()">▶️ Play</button>
|
|
<button id="muteBtn" onclick="toggleMute()">🔊 Mute</button>
|
|
<button onclick="closeVideo()">↩️ Back</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CONFIGURATION
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
const API_BASE_URL = 'https://hotspots.fabio.ovh';
|
|
|
|
// Video base URL (configure based on where videos are hosted)
|
|
const VIDEO_BASE_URL = ''; // Leave empty for local/relative paths
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// STATE
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
let currentLanguage = 'en';
|
|
let currentContent = null;
|
|
let isPlaying = false;
|
|
let tesseractWorker = null;
|
|
let ocrReady = false;
|
|
let apiReady = false;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// API FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
async function checkApiHealth() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/health`);
|
|
const data = await response.json();
|
|
apiReady = data.status === 'healthy' && data.database_loaded;
|
|
updateApiStatus(apiReady);
|
|
console.log('✅ API Health:', data);
|
|
return apiReady;
|
|
} catch (err) {
|
|
console.error('❌ API Health check failed:', err);
|
|
apiReady = false;
|
|
updateApiStatus(false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function matchTextWithApi(text) {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/match`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
text: text,
|
|
language: currentLanguage
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('API Match result:', data);
|
|
return data;
|
|
} catch (err) {
|
|
console.error('API Match error:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function updateApiStatus(ready) {
|
|
const dot = document.getElementById('apiDot');
|
|
const status = document.getElementById('apiStatus');
|
|
if (ready) {
|
|
dot.classList.add('ready');
|
|
status.textContent = 'Ready';
|
|
} else {
|
|
dot.classList.remove('ready');
|
|
status.textContent = 'Offline';
|
|
}
|
|
updateCaptureButton();
|
|
}
|
|
|
|
function updateOcrStatus(ready) {
|
|
const dot = document.getElementById('ocrDot');
|
|
const status = document.getElementById('ocrStatus');
|
|
if (ready) {
|
|
dot.classList.add('ready');
|
|
status.textContent = 'Ready';
|
|
} else {
|
|
dot.classList.remove('ready');
|
|
status.textContent = 'Loading...';
|
|
}
|
|
updateCaptureButton();
|
|
}
|
|
|
|
function updateCaptureButton() {
|
|
const btn = document.getElementById('captureBtn');
|
|
btn.disabled = !(ocrReady && apiReady);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// OCR FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
async function initOCR() {
|
|
try {
|
|
console.log('=== TESSERACT INITIALIZATION START ===');
|
|
tesseractWorker = await Tesseract.createWorker();
|
|
await tesseractWorker.loadLanguage('eng');
|
|
await tesseractWorker.initialize('eng');
|
|
ocrReady = true;
|
|
updateOcrStatus(true);
|
|
console.log('✅ TESSERACT FULLY READY');
|
|
} catch (err) {
|
|
console.error('❌ OCR initialization failed:', err);
|
|
ocrReady = false;
|
|
updateOcrStatus(false);
|
|
showError('OCR initialization failed: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CAMERA FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
async function initCamera() {
|
|
try {
|
|
console.log('=== CAMERA INITIALIZATION ===');
|
|
const constraints = {
|
|
video: {
|
|
facingMode: 'environment',
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 }
|
|
}
|
|
};
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
const video = document.getElementById('video');
|
|
video.srcObject = stream;
|
|
|
|
video.onloadedmetadata = () => {
|
|
video.play();
|
|
console.log('✅ Camera ready');
|
|
};
|
|
|
|
// Hide permission button after success
|
|
document.getElementById('permissionBtn').style.display = 'none';
|
|
|
|
} catch (err) {
|
|
console.error('Camera error:', err);
|
|
if (err.name === 'NotAllowedError') {
|
|
showError('❌ Camera permission denied. Please allow camera access.');
|
|
} else if (err.name === 'NotFoundError') {
|
|
showError('❌ No camera found on this device.');
|
|
} else {
|
|
showError('❌ Camera access failed: ' + err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CAPTURE & RECOGNIZE
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
async function captureAndRecognize() {
|
|
if (!tesseractWorker || !apiReady) {
|
|
showError('OCR or API not ready. Please wait...');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('captureBtn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner"></span> Recognizing...';
|
|
|
|
currentLanguage = document.getElementById('language').value;
|
|
|
|
try {
|
|
const video = document.getElementById('video');
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
if (video.readyState !== 4 || video.videoWidth === 0) {
|
|
showError('Camera not ready. Try again.');
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
ctx.drawImage(video, 0, 0);
|
|
|
|
// OCR
|
|
btn.innerHTML = '<span class="spinner"></span> Reading text...';
|
|
const result = await tesseractWorker.recognize(canvas);
|
|
const { data: { text, confidence } } = result;
|
|
const recognizedText = text.trim();
|
|
|
|
console.log('Recognized:', recognizedText, 'Confidence:', confidence);
|
|
|
|
if (!recognizedText) {
|
|
showNoTextFound();
|
|
return;
|
|
}
|
|
|
|
// Show recognized text
|
|
showRecognizedText(recognizedText, confidence);
|
|
|
|
// Match with API
|
|
btn.innerHTML = '<span class="spinner"></span> Matching...';
|
|
const matchResult = await matchTextWithApi(recognizedText);
|
|
|
|
if (matchResult.found) {
|
|
showMatchFound(matchResult, recognizedText, confidence);
|
|
// Auto-load content after short delay
|
|
setTimeout(() => loadContent(matchResult), 1500);
|
|
} else {
|
|
showContentNotFound(recognizedText);
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Recognition error:', err);
|
|
showError('Recognition failed: ' + err.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '📷 Capture & Recognize';
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// UI FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function showRecognizedText(text, confidence) {
|
|
const box = document.getElementById('resultBox');
|
|
const label = document.getElementById('resultLabel');
|
|
const resultText = document.getElementById('resultText');
|
|
const confidenceEl = document.getElementById('confidence');
|
|
const errorMsg = document.getElementById('errorMessage');
|
|
const matchDetails = document.getElementById('matchDetails');
|
|
|
|
box.classList.remove('hidden', 'error', 'not-found', 'success');
|
|
label.textContent = '🔍 Text Recognized';
|
|
resultText.textContent = text;
|
|
confidenceEl.textContent = `OCR Confidence: ${confidence.toFixed(1)}%`;
|
|
matchDetails.textContent = 'Searching database...';
|
|
errorMsg.textContent = '';
|
|
}
|
|
|
|
function showMatchFound(match, recognizedText, ocrConfidence) {
|
|
const box = document.getElementById('resultBox');
|
|
const label = document.getElementById('resultLabel');
|
|
const resultText = document.getElementById('resultText');
|
|
const matchDetails = document.getElementById('matchDetails');
|
|
const confidenceEl = document.getElementById('confidence');
|
|
|
|
box.classList.remove('error', 'not-found');
|
|
box.classList.add('success');
|
|
label.textContent = '✅ Match Found!';
|
|
resultText.textContent = match.name;
|
|
matchDetails.textContent = `Hotspot ID: ${match.hotspot_id}`;
|
|
confidenceEl.textContent = `OCR: ${ocrConfidence.toFixed(1)}% | Match: ${(match.confidence * 100).toFixed(0)}%`;
|
|
|
|
currentContent = match;
|
|
}
|
|
|
|
function showNoTextFound() {
|
|
const box = document.getElementById('resultBox');
|
|
const label = document.getElementById('resultLabel');
|
|
const resultText = document.getElementById('resultText');
|
|
const errorMsg = document.getElementById('errorMessage');
|
|
const matchDetails = document.getElementById('matchDetails');
|
|
|
|
box.classList.remove('hidden', 'success', 'not-found');
|
|
box.classList.add('error');
|
|
label.textContent = '⚠️ No Text Detected';
|
|
resultText.textContent = '';
|
|
matchDetails.textContent = '';
|
|
errorMsg.textContent = 'No readable text found. Try pointing at clear, well-lit text.';
|
|
}
|
|
|
|
function showContentNotFound(recognizedText) {
|
|
const box = document.getElementById('resultBox');
|
|
const label = document.getElementById('resultLabel');
|
|
const resultText = document.getElementById('resultText');
|
|
const resultStatus = document.getElementById('resultStatus');
|
|
const errorMsg = document.getElementById('errorMessage');
|
|
const matchDetails = document.getElementById('matchDetails');
|
|
|
|
box.classList.remove('hidden', 'success', 'error');
|
|
box.classList.add('not-found');
|
|
label.textContent = '❌ No Match Found';
|
|
resultText.textContent = recognizedText;
|
|
resultStatus.textContent = '';
|
|
matchDetails.textContent = 'Text does not match any hotspot in database';
|
|
errorMsg.textContent = 'Try: Brussels, Paris, Berlin, Rome...';
|
|
}
|
|
|
|
function showError(message) {
|
|
const box = document.getElementById('resultBox');
|
|
const label = document.getElementById('resultLabel');
|
|
const resultText = document.getElementById('resultText');
|
|
const errorMsg = document.getElementById('errorMessage');
|
|
const matchDetails = document.getElementById('matchDetails');
|
|
|
|
box.classList.remove('hidden', 'success', 'not-found');
|
|
box.classList.add('error');
|
|
label.textContent = '⚠️ Error';
|
|
resultText.textContent = '';
|
|
matchDetails.textContent = '';
|
|
errorMsg.textContent = message;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// VIDEO PLAYBACK
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function loadContent(match) {
|
|
const videoPlayer = document.getElementById('videoPlayer');
|
|
const videoTitle = document.getElementById('videoTitle');
|
|
const videoMeta = document.getElementById('videoMeta');
|
|
const infoBox = document.getElementById('infoBox');
|
|
|
|
videoTitle.textContent = match.name;
|
|
videoMeta.textContent = `Hotspot ID: ${match.hotspot_id}`;
|
|
|
|
infoBox.innerHTML = `
|
|
<strong>${match.name}</strong><br>
|
|
ID: ${match.hotspot_id}<br>
|
|
Match: ${(match.confidence * 100).toFixed(0)}%<br>
|
|
${match.video_ids ? `Videos: ${match.video_ids.join(', ')}` : 'No video mapped'}
|
|
`;
|
|
|
|
// Try to load video if video_ids exist
|
|
if (match.video_ids && match.video_ids.length > 0) {
|
|
const videoId = match.video_ids[0];
|
|
const videoUrl = VIDEO_BASE_URL ? `${VIDEO_BASE_URL}/${videoId}.mp4` : `${videoId}.mp4`;
|
|
|
|
videoPlayer.src = videoUrl;
|
|
videoPlayer.onerror = () => {
|
|
// Fallback to sample video
|
|
console.log('Video not found, using sample');
|
|
videoPlayer.src = 'https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4';
|
|
};
|
|
} else {
|
|
// Use sample video
|
|
videoPlayer.src = 'https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4';
|
|
}
|
|
|
|
videoPlayer.play();
|
|
isPlaying = true;
|
|
updatePlayPauseButton();
|
|
|
|
document.getElementById('captureMode').classList.add('hidden');
|
|
document.getElementById('playingMode').classList.remove('hidden');
|
|
}
|
|
|
|
function closeVideo() {
|
|
document.getElementById('videoPlayer').pause();
|
|
document.getElementById('playingMode').classList.add('hidden');
|
|
document.getElementById('captureMode').classList.remove('hidden');
|
|
document.getElementById('resultBox').classList.add('hidden');
|
|
currentContent = null;
|
|
isPlaying = false;
|
|
}
|
|
|
|
function togglePlayPause() {
|
|
const video = document.getElementById('videoPlayer');
|
|
if (isPlaying) {
|
|
video.pause();
|
|
} else {
|
|
video.play();
|
|
}
|
|
isPlaying = !isPlaying;
|
|
updatePlayPauseButton();
|
|
}
|
|
|
|
function updatePlayPauseButton() {
|
|
const btn = document.getElementById('playPauseBtn');
|
|
btn.textContent = isPlaying ? '⏸️ Pause' : '▶️ Play';
|
|
}
|
|
|
|
function toggleMute() {
|
|
const video = document.getElementById('videoPlayer');
|
|
const btn = document.getElementById('muteBtn');
|
|
video.muted = !video.muted;
|
|
btn.textContent = video.muted ? '🔇 Unmute' : '🔊 Mute';
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// INITIALIZATION
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
console.log('=== APP INITIALIZATION ===');
|
|
|
|
// Language change handler
|
|
document.getElementById('language').addEventListener('change', (e) => {
|
|
currentLanguage = e.target.value;
|
|
});
|
|
|
|
// Initialize in parallel
|
|
await Promise.all([
|
|
initOCR(),
|
|
checkApiHealth()
|
|
]);
|
|
|
|
// Auto-init camera on mobile
|
|
if (/Android|iPhone|iPad/i.test(navigator.userAgent)) {
|
|
initCamera();
|
|
}
|
|
|
|
console.log('=== APP READY ===');
|
|
});
|
|
|
|
// Cleanup
|
|
window.addEventListener('beforeunload', async () => {
|
|
if (tesseractWorker) {
|
|
await tesseractWorker.terminate();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |