Add service worker for push notifications, create calendar layout, and implement WLAN QR code page
- Implemented a service worker (sw.js) to handle push notifications with dynamic options and notification click events. - Created a calendar layout in test.html with a grid system for displaying events across days and times. - Developed a visually engaging WLAN QR code page (wlan.html) with animated backgrounds, particle effects, and tips for connecting to the network.
This commit is contained in:
199
public/led/v.html
Normal file
199
public/led/v.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Video Frame-Stepping mit FPS-Erkennung</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
||||
padding: 18px;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #ddd;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 10px;
|
||||
color: #444;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Video: Frame für Frame mit FPS-Erkennung</h1>
|
||||
|
||||
<div class="controls">
|
||||
<label>Wähle Datei:
|
||||
<input id="file" type="file" accept="video/*">
|
||||
</label>
|
||||
|
||||
<label>fps:
|
||||
<input id="fps" type="number" step="0.01" value="25" min="1">
|
||||
</label>
|
||||
|
||||
<button id="prev">◀️ Prev Frame</button>
|
||||
<button id="next">Next Frame ▶️</button>
|
||||
<button id="play">Play ▶</button>
|
||||
<button id="pause">Pause ⏸</button>
|
||||
<button id="export">Export Current Frame (PNG)</button>
|
||||
</div>
|
||||
|
||||
<video id="video" controls crossorigin="anonymous" style="display:none"></video>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="info" id="info">
|
||||
Lade ein Video, warte auf Metadaten...
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('file');
|
||||
const video = document.getElementById('video');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const fpsInput = document.getElementById('fps');
|
||||
const prevBtn = document.getElementById('prev');
|
||||
const nextBtn = document.getElementById('next');
|
||||
const playBtn = document.getElementById('play');
|
||||
const pauseBtn = document.getElementById('pause');
|
||||
const exportBtn = document.getElementById('export');
|
||||
const info = document.getElementById('info');
|
||||
|
||||
let fileURL = null;
|
||||
let rafId = null;
|
||||
|
||||
// --- Datei laden ---
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
if (fileURL) URL.revokeObjectURL(fileURL);
|
||||
fileURL = URL.createObjectURL(file);
|
||||
video.src = fileURL;
|
||||
video.style.display = 'block';
|
||||
video.load();
|
||||
info.textContent = 'Video geladen. Warte auf Metadaten...';
|
||||
});
|
||||
|
||||
// --- Metadaten geladen ---
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 360;
|
||||
seekAndDraw(0).catch(() => { });
|
||||
info.textContent = `Dauer: ${formatSeconds(video.duration)} — Bildgröße: ${canvas.width}×${canvas.height}`;
|
||||
detectFPS();
|
||||
});
|
||||
|
||||
// --- Hilfsfunktionen ---
|
||||
function formatSeconds(s) {
|
||||
if (!isFinite(s)) return '–';
|
||||
const mm = Math.floor(s / 60);
|
||||
const ss = (s % 60).toFixed(2).padStart(5, '0');
|
||||
return `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function drawCurrentFrame() {
|
||||
try {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
} catch (e) {
|
||||
console.warn('drawImage fehlgeschlagen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function seek(videoEl, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function cleanup() {
|
||||
videoEl.removeEventListener('seeked', onSeeked);
|
||||
videoEl.removeEventListener('error', onError);
|
||||
}
|
||||
function onSeeked() { cleanup(); resolve(); }
|
||||
function onError(e) { cleanup(); reject(e); }
|
||||
videoEl.addEventListener('seeked', onSeeked);
|
||||
videoEl.addEventListener('error', onError);
|
||||
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
|
||||
});
|
||||
}
|
||||
|
||||
async function seekAndDraw(time) {
|
||||
await seek(video, time);
|
||||
drawCurrentFrame();
|
||||
}
|
||||
|
||||
|
||||
// --- Buttons ---
|
||||
nextBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) + frameDur;
|
||||
await seekAndDraw(Math.min(target + 0.00001, video.duration || target));
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) - frameDur;
|
||||
await seekAndDraw(Math.max(target, 0));
|
||||
});
|
||||
|
||||
|
||||
exportBtn.addEventListener('click', () => {
|
||||
drawCurrentFrame();
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = 'frame.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||
}, 'image/png');
|
||||
});
|
||||
|
||||
// --- Seeked Event ---
|
||||
video.addEventListener('seeked', () => { drawCurrentFrame(); });
|
||||
|
||||
// --- Safety Initial Draw ---
|
||||
window.addEventListener('load', () => {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 360);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user