first commit
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>Screen Cast</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0; padding: 0; height: 100%; width: 100%;
|
||||
background: #000; overflow: hidden; cursor: none;
|
||||
}
|
||||
#screen {
|
||||
position: fixed; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: contain; /* ganzer Bildschirm sichtbar */
|
||||
background: #000;
|
||||
}
|
||||
#screen.fill { object-fit: cover; } /* randlos, schneidet Ränder ab */
|
||||
#overlay {
|
||||
position: fixed; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
background: rgba(0,0,0,.86);
|
||||
color: #fff; font-family: system-ui, "Segoe UI", Roboto, sans-serif;
|
||||
text-align: center; transition: opacity .4s; z-index: 10;
|
||||
}
|
||||
#overlay.hidden { opacity: 0; pointer-events: none; }
|
||||
#title { font-size: 5vh; font-weight: 700; letter-spacing: .5px; }
|
||||
#status { margin-top: 2vh; font-size: 2.6vh; opacity: .8; }
|
||||
#keys { margin-top: 5vh; font-size: 2vh; opacity: .5; line-height: 1.9; }
|
||||
.dot {
|
||||
display: inline-block; width: 1.4vh; height: 1.4vh; border-radius: 50%;
|
||||
background: #ff5252; margin-right: 1vh; animation: pulse 1.2s infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity: .25; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="screen" autoplay muted playsinline></video>
|
||||
|
||||
<div id="overlay">
|
||||
<div id="title">Screen Cast</div>
|
||||
<div id="status"><span class="dot"></span>Verbinde …</div>
|
||||
<div id="keys">OK / Klick = Vollbild · F = Bild füllen oder einpassen</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var video = document.getElementById('screen');
|
||||
var overlay = document.getElementById('overlay');
|
||||
var status = document.getElementById('status');
|
||||
|
||||
var ws = null, mediaSource = null, sourceBuffer = null;
|
||||
var queue = [], mimeType = '', gotFirstFrame = false;
|
||||
var hideTimer = null, retryDelay = 700;
|
||||
|
||||
function say(text, sticky) {
|
||||
status.innerHTML = '<span class="dot"></span>' + text;
|
||||
overlay.classList.remove('hidden');
|
||||
clearTimeout(hideTimer);
|
||||
if (!sticky) hideTimer = setTimeout(function () {
|
||||
overlay.classList.add('hidden');
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- MSE
|
||||
|
||||
function resetPlayer(mime) {
|
||||
mimeType = mime || mimeType;
|
||||
queue = [];
|
||||
gotFirstFrame = false;
|
||||
if (sourceBuffer) { try { mediaSource.removeSourceBuffer(sourceBuffer); } catch (e) {} }
|
||||
sourceBuffer = null;
|
||||
mediaSource = new MediaSource();
|
||||
video.src = URL.createObjectURL(mediaSource);
|
||||
mediaSource.addEventListener('sourceopen', function () {
|
||||
try {
|
||||
sourceBuffer = mediaSource.addSourceBuffer(mimeType);
|
||||
sourceBuffer.mode = 'segments';
|
||||
sourceBuffer.addEventListener('updateend', pump);
|
||||
pump();
|
||||
} catch (e) {
|
||||
say('Dieser Fernseher kann das Videoformat nicht abspielen (' + mimeType + ')', true);
|
||||
}
|
||||
});
|
||||
video.play().catch(function () {});
|
||||
}
|
||||
|
||||
function pump() {
|
||||
if (!sourceBuffer || sourceBuffer.updating || queue.length === 0) return;
|
||||
// Bei Rückstand die ältesten Fragmente überspringen: lieber ein Sprung
|
||||
// als eine wachsende Verzögerung.
|
||||
if (queue.length > 60) queue.splice(0, queue.length - 20);
|
||||
var chunk = queue.shift();
|
||||
try {
|
||||
sourceBuffer.appendBuffer(chunk);
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
trimBuffer(true);
|
||||
queue.unshift(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Alte Daten wegwerfen, sonst läuft der Speicher des Fernsehers voll. */
|
||||
function trimBuffer(aggressive) {
|
||||
if (!sourceBuffer || sourceBuffer.updating) return;
|
||||
try {
|
||||
var buffered = sourceBuffer.buffered;
|
||||
if (!buffered.length) return;
|
||||
var keep = aggressive ? 2 : 12;
|
||||
var cutoff = video.currentTime - keep;
|
||||
if (cutoff > buffered.start(0) + 0.5) sourceBuffer.remove(buffered.start(0), cutoff);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** Hält die Verzögerung klein: notfalls ans Live-Ende springen. */
|
||||
function keepLive() {
|
||||
if (!sourceBuffer || !video.buffered.length) return;
|
||||
var end = video.buffered.end(video.buffered.length - 1);
|
||||
var behind = end - video.currentTime;
|
||||
if (behind > 2.5 || video.currentTime === 0) {
|
||||
video.currentTime = Math.max(0, end - 0.2);
|
||||
video.playbackRate = 1.0;
|
||||
} else if (behind > 0.7) {
|
||||
video.playbackRate = 1.08; // sanft aufholen statt springen
|
||||
} else {
|
||||
video.playbackRate = 1.0;
|
||||
}
|
||||
if (video.paused) video.play().catch(function () {});
|
||||
}
|
||||
|
||||
setInterval(function () { trimBuffer(false); keepLive(); }, 1000);
|
||||
|
||||
// --------------------------------------------------------- Verbindung
|
||||
|
||||
function connect() {
|
||||
var proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(proto + location.host + '/ws');
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = function () {
|
||||
retryDelay = 700;
|
||||
say('Verbunden – warte auf Bild …', true);
|
||||
};
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
if (typeof event.data === 'string') {
|
||||
var msg = JSON.parse(event.data);
|
||||
if (msg.type === 'init') {
|
||||
var mime = 'video/mp4; codecs="' + msg.codec + '"';
|
||||
if (window.MediaSource && !MediaSource.isTypeSupported(mime)) {
|
||||
mime = 'video/mp4; codecs="avc1.42E01E"'; // Notfall-Fallback
|
||||
}
|
||||
resetPlayer(mime);
|
||||
} else if (msg.type === 'reset') {
|
||||
resetPlayer(mimeType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
queue.push(new Uint8Array(event.data));
|
||||
if (!gotFirstFrame) {
|
||||
gotFirstFrame = true;
|
||||
say('Los geht’s', false);
|
||||
}
|
||||
pump();
|
||||
};
|
||||
|
||||
ws.onclose = function () {
|
||||
say('Verbindung getrennt – neuer Versuch …', true);
|
||||
setTimeout(connect, retryDelay);
|
||||
retryDelay = Math.min(retryDelay * 1.6, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = function () { try { ws.close(); } catch (e) {} };
|
||||
}
|
||||
|
||||
if (!window.MediaSource) {
|
||||
say('Dieser Browser unterstützt kein Live-Video. Versuche stattdessen ' +
|
||||
location.origin + '/stream.ts in VLC.', true);
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- Bedienung
|
||||
|
||||
function goFullscreen() {
|
||||
var el = document.documentElement;
|
||||
var request = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
|
||||
if (request) request.call(el).catch(function () {});
|
||||
}
|
||||
|
||||
function toggleFill() {
|
||||
video.classList.toggle('fill');
|
||||
say(video.classList.contains('fill') ? 'Bild füllt den Schirm' : 'Ganzes Bild sichtbar', false);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function () { goFullscreen(); video.play().catch(function(){}); });
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'f' || e.key === 'F') { toggleFill(); return; }
|
||||
if (e.key === 'Enter' || e.keyCode === 13) { goFullscreen(); }
|
||||
video.play().catch(function () {});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user