This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/h5p-service/players/interactive-video-player.html
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

426 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.0">
<title>Interactive Video - BreakPilot H5P</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
}
.container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 12px;
font-size: 28px;
}
.description {
color: #6b7280;
margin-bottom: 24px;
font-size: 16px;
line-height: 1.6;
}
.video-wrapper {
position: relative;
background: #000;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16/9;
margin-bottom: 24px;
}
.video-wrapper iframe,
.video-wrapper video {
width: 100%;
height: 100%;
border: none;
}
.interaction-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: none;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 100;
}
.interaction-overlay.show {
display: flex;
}
.interaction-content {
background: white;
border-radius: 12px;
padding: 32px;
max-width: 600px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.interaction-icon {
font-size: 48px;
text-align: center;
margin-bottom: 16px;
}
.interaction-title {
font-size: 24px;
font-weight: 700;
color: #1f2937;
margin-bottom: 16px;
text-align: center;
}
.interaction-text {
color: #4b5563;
font-size: 16px;
line-height: 1.6;
margin-bottom: 24px;
}
.interaction-link {
display: block;
padding: 12px 24px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
font-weight: 600;
margin-bottom: 12px;
}
.interaction-link:hover {
background: #5568d3;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
width: 100%;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.timeline {
background: #f9fafb;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.timeline-title {
font-weight: 700;
color: #374151;
margin-bottom: 12px;
font-size: 14px;
}
.timeline-markers {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.timeline-marker {
background: #fef3c7;
border: 2px solid #f59e0b;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
color: #92400e;
cursor: pointer;
transition: all 0.2s;
}
.timeline-marker:hover {
background: #fde68a;
transform: translateY(-2px);
}
.timeline-marker.completed {
background: #d1fae5;
border-color: #10b981;
color: #065f46;
}
.controls {
text-align: center;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1 id="title"></h1>
<p class="description" id="description"></p>
<div class="video-wrapper">
<div id="videoContainer"></div>
<div class="interaction-overlay" id="interactionOverlay">
<div class="interaction-content">
<div class="interaction-icon" id="interactionIcon"></div>
<div class="interaction-title" id="interactionTitle"></div>
<div class="interaction-text" id="interactionText"></div>
<div id="interactionExtra"></div>
<button class="btn btn-primary" onclick="closeInteraction()">Weiter</button>
</div>
</div>
</div>
<div class="timeline">
<div class="timeline-title">📌 Interaktive Elemente im Video:</div>
<div class="timeline-markers" id="timelineMarkers"></div>
</div>
<div class="controls">
Das Video pausiert automatisch bei interaktiven Elementen
</div>
</div>
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://player.vimeo.com/api/player.js"></script>
<script>
let data = null;
let player = null;
let playerType = null;
let currentTime = 0;
let completedInteractions = new Set();
let checkInterval = null;
function loadContent() {
const urlParams = new URLSearchParams(window.location.search);
const dataParam = urlParams.get('data');
if (dataParam) {
try {
data = JSON.parse(decodeURIComponent(dataParam));
} catch (e) {
alert('Fehler beim Laden!');
return;
}
}
if (!data) {
alert('Keine Daten gefunden!');
return;
}
document.getElementById('title').textContent = data.title;
document.getElementById('description').textContent = data.description || '';
renderTimeline();
embedVideo();
}
function embedVideo() {
const container = document.getElementById('videoContainer');
const url = data.videoUrl;
// YouTube
if (url.includes('youtube.com') || url.includes('youtu.be')) {
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)?.[1];
if (videoId) {
playerType = 'youtube';
container.innerHTML = `<div id="ytPlayer"></div>`;
// YouTube API will be loaded via script tag
window.onYouTubeIframeAPIReady = function() {
player = new YT.Player('ytPlayer', {
videoId: videoId,
width: '100%',
height: '100%',
playerVars: {
autoplay: 0,
modestbranding: 1,
rel: 0
},
events: {
onReady: function() {
startTimeTracking();
},
onStateChange: function(event) {
if (event.data === YT.PlayerState.PLAYING) {
startTimeTracking();
}
}
}
});
};
}
}
// Vimeo
else if (url.includes('vimeo.com')) {
const videoId = url.match(/vimeo\.com\/(\d+)/)?.[1];
if (videoId) {
playerType = 'vimeo';
container.innerHTML = `<iframe id="vimeoPlayer" src="https://player.vimeo.com/video/${videoId}" allowfullscreen></iframe>`;
setTimeout(() => {
player = new Vimeo.Player('vimeoPlayer');
player.on('timeupdate', function(data) {
currentTime = data.seconds;
checkInteractions();
});
}, 500);
}
}
// Direct MP4
else if (url.endsWith('.mp4')) {
playerType = 'html5';
container.innerHTML = `<video id="html5Player" controls style="width: 100%; height: 100%;"><source src="${url}" type="video/mp4"></video>`;
const video = document.getElementById('html5Player');
video.addEventListener('timeupdate', function() {
currentTime = video.currentTime;
checkInteractions();
});
player = video;
}
}
function startTimeTracking() {
if (checkInterval) clearInterval(checkInterval);
checkInterval = setInterval(() => {
if (playerType === 'youtube' && player && player.getCurrentTime) {
currentTime = player.getCurrentTime();
checkInteractions();
}
}, 500);
}
function checkInteractions() {
data.interactions.forEach((interaction, index) => {
const interactionTime = timeToSeconds(interaction.time);
// Check if we're at this interaction time (within 1 second window)
if (Math.abs(currentTime - interactionTime) < 0.5 && !completedInteractions.has(index)) {
showInteraction(interaction, index);
}
});
}
function showInteraction(interaction, index) {
// Pause video
if (playerType === 'youtube' && player && player.pauseVideo) {
player.pauseVideo();
} else if (playerType === 'vimeo' && player && player.pause) {
player.pause();
} else if (playerType === 'html5' && player && player.pause) {
player.pause();
}
// Show overlay
const overlay = document.getElementById('interactionOverlay');
const icon = document.getElementById('interactionIcon');
const title = document.getElementById('interactionTitle');
const text = document.getElementById('interactionText');
const extra = document.getElementById('interactionExtra');
// Set icon based on type
switch (interaction.type) {
case 'question':
icon.textContent = '❓';
break;
case 'info':
icon.textContent = '';
break;
case 'link':
icon.textContent = '🔗';
break;
}
title.textContent = interaction.title;
text.textContent = interaction.content;
// Add link if type is link
if (interaction.type === 'link') {
extra.innerHTML = `<a href="${interaction.content}" target="_blank" class="interaction-link">🔗 Link öffnen</a>`;
} else {
extra.innerHTML = '';
}
overlay.classList.add('show');
completedInteractions.add(index);
// Update timeline
renderTimeline();
}
function closeInteraction() {
const overlay = document.getElementById('interactionOverlay');
overlay.classList.remove('show');
// Resume video
if (playerType === 'youtube' && player && player.playVideo) {
player.playVideo();
} else if (playerType === 'vimeo' && player && player.play) {
player.play();
} else if (playerType === 'html5' && player && player.play) {
player.play();
}
}
function renderTimeline() {
const container = document.getElementById('timelineMarkers');
container.innerHTML = '';
data.interactions.forEach((interaction, index) => {
const marker = document.createElement('div');
marker.className = 'timeline-marker';
if (completedInteractions.has(index)) {
marker.classList.add('completed');
}
const iconMap = {
question: '❓',
info: '',
link: '🔗'
};
marker.textContent = `${iconMap[interaction.type]} ${interaction.time} - ${interaction.title}`;
marker.onclick = () => seekTo(interaction.time);
container.appendChild(marker);
});
}
function seekTo(timeStr) {
const seconds = timeToSeconds(timeStr);
if (playerType === 'youtube' && player && player.seekTo) {
player.seekTo(seconds, true);
} else if (playerType === 'vimeo' && player && player.setCurrentTime) {
player.setCurrentTime(seconds);
} else if (playerType === 'html5' && player) {
player.currentTime = seconds;
}
}
function timeToSeconds(timeStr) {
const parts = timeStr.split(':');
const minutes = parseInt(parts[0]) || 0;
const seconds = parseInt(parts[1]) || 0;
return minutes * 60 + seconds;
}
loadContent();
</script>
</body>
</html>