Files
fsae41.de/public/led/v.html
BolkeDerBaer 038910e9f0 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.
2026-02-22 00:50:22 +01:00

199 lines
6.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>