Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## [Unreleased]

### Added
- Converted event video teaser to VP9 WebM format (`img/irrer_wissenschaftler.webm`) with a static WebP poster frame (`img/irrer_wissenschaftler_poster.webp`) for instant buffering and ~70% bandwidth savings.
- Enhanced event video player rendering on `index.html` and `events.html` with accessible `aria-label` attributes and `preload="metadata"`.
- Added `@media (prefers-reduced-motion: reduce)` CSS rules to disable auto-playing video teasers for users who prefer reduced motion.
- Added `scripts/Convert-ToWebM.ps1` PowerShell automation script for converting MP4 videos to WebM and generating poster frames.
- Added square modern PWA and apple-touch-icon (`img/psugh-icon.webp`) in WebP format to resolve PWA verification warnings and fix icon squashing on iOS.
- Added preloading for hero background image `img/hintergrund.webp` and minified styles on `index.html`, `events.html`, and `impressum.html` to improve Largest Contentful Paint (LCP) performance.
- Added preloading for JSON data files on `events.html` to load event data as early as possible.
Expand Down
8 changes: 8 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1518,3 +1518,11 @@ button:focus {
.comparison-table tr:hover {
background: var(--bg-secondary);
}

@media (prefers-reduced-motion: reduce) {
.topic-picture video,
video.topic-picture {
display: none;
}
}

2 changes: 1 addition & 1 deletion css/styles.min.css

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion current-meeting.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
{
"title": "Besser Skripten mit dem PSScriptAnalyzer",
"speaker": "Christian Ritter",
"video": "img/irrer_wissenschaftler.mp4",
"video": {
"webm": "img/irrer_wissenschaftler.webm",
"mp4": "img/irrer_wissenschaftler.mp4",
"poster": "img/irrer_wissenschaftler_poster.webp"
},
"description": "Regeln, überall nur Regeln!\n\nOb im Alltag oder beim Entwickeln – Regeln geben uns Orientierung und helfen, Qualität zu sichern. In PowerShell begegnen sie uns als PSScriptAnalyzer Rules, die unseren Code konsistent, sauber und wartbar halten.\n\nIn dieser Session zeigt euch Christian Ritter, wie PSScriptAnalyzer-Regeln funktionieren, wie ihr eigene Regeln erstellt und wie ihr diese mithilfe von CI/CD-Pipelines automatisiert durchsetzen könnt. Freut euch auf praxisnahe Einblicke, Beispiele aus dem Entwickleralltag und wertvolle Tipps für euer tägliches PowerShell-Coding."
}
]
Expand Down
37 changes: 29 additions & 8 deletions events.html
Original file line number Diff line number Diff line change
Expand Up @@ -489,23 +489,44 @@ <h2 class="section-title">Vergangene Treffen</h2>

topicElement.appendChild(topicDescription);

const videoPath = topic.video || (topic.picture && topic.picture.match(/\.(mp4|webm)$/i) ? topic.picture : null);
const videoObj = (typeof topic.video === 'object') ? topic.video : null;
const videoPath = videoObj ? null : (topic.video || (topic.picture && topic.picture.match(/\.(mp4|webm)$/i) ? topic.picture : null));
const picturePath = topic.picture;

if (videoPath) {
if (videoObj || videoPath) {
const video = document.createElement('video');
video.className = 'topic-picture';
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
if (picturePath && !picturePath.match(/\.(mp4|webm)$/i)) {
video.poster = picturePath;
video.setAttribute('preload', 'metadata');
video.setAttribute('aria-label', topic.title ? `Video-Teaser: ${topic.title}` : 'Themenvideo');

const posterUrl = (videoObj && videoObj.poster) || (picturePath && !picturePath.match(/\.(mp4|webm)$/i) ? picturePath : null);
if (posterUrl) {
video.poster = posterUrl;
}

if (videoObj) {
if (videoObj.webm) {
const sourceWebm = document.createElement('source');
sourceWebm.src = videoObj.webm;
sourceWebm.type = 'video/webm';
video.appendChild(sourceWebm);
}
if (videoObj.mp4) {
const sourceMp4 = document.createElement('source');
sourceMp4.src = videoObj.mp4;
sourceMp4.type = 'video/mp4';
video.appendChild(sourceMp4);
}
} else {
const source = document.createElement('source');
source.src = videoPath;
source.type = videoPath.endsWith('.webm') ? 'video/webm' : 'video/mp4';
video.appendChild(source);
}
const source = document.createElement('source');
source.src = videoPath;
source.type = videoPath.endsWith('.webm') ? 'video/webm' : 'video/mp4';
video.appendChild(source);
topicElement.appendChild(video);
} else if (picturePath) {
const picture = document.createElement('img');
Expand Down
Binary file added img/irrer_wissenschaftler.webm
Binary file not shown.
Binary file added img/irrer_wissenschaftler_poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 29 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -525,23 +525,44 @@ <h3>Newsletter abonnieren</h3>
description.innerHTML = topic.description.replace(/\n/g, '<br>');
topicCard.appendChild(description);

const videoPath = topic.video || meeting.video || (topic.picture && topic.picture.match(/\.(mp4|webm)$/i) ? topic.picture : null);
const videoObj = (typeof topic.video === 'object') ? topic.video : (typeof meeting.video === 'object' ? meeting.video : null);
const videoPath = videoObj ? null : (topic.video || meeting.video || (topic.picture && topic.picture.match(/\.(mp4|webm)$/i) ? topic.picture : null));
const picturePath = topic.picture || meeting.picture;

if (videoPath) {
if (videoObj || videoPath) {
const video = document.createElement('video');
video.className = 'topic-picture';
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
if (picturePath && !picturePath.match(/\.(mp4|webm)$/i)) {
video.poster = picturePath;
video.setAttribute('preload', 'metadata');
video.setAttribute('aria-label', topic.title ? `Video-Teaser: ${topic.title}` : 'Themenvideo');

const posterUrl = (videoObj && videoObj.poster) || (picturePath && !picturePath.match(/\.(mp4|webm)$/i) ? picturePath : null);
if (posterUrl) {
video.poster = posterUrl;
}

if (videoObj) {
if (videoObj.webm) {
const sourceWebm = document.createElement('source');
sourceWebm.src = videoObj.webm;
sourceWebm.type = 'video/webm';
video.appendChild(sourceWebm);
}
if (videoObj.mp4) {
const sourceMp4 = document.createElement('source');
sourceMp4.src = videoObj.mp4;
sourceMp4.type = 'video/mp4';
video.appendChild(sourceMp4);
}
} else {
const source = document.createElement('source');
source.src = videoPath;
source.type = videoPath.endsWith('.webm') ? 'video/webm' : 'video/mp4';
video.appendChild(source);
}
const source = document.createElement('source');
source.src = videoPath;
source.type = videoPath.endsWith('.webm') ? 'video/webm' : 'video/mp4';
video.appendChild(source);
topicCard.appendChild(video);
} else if (picturePath) {
const picture = document.createElement('img');
Expand Down
67 changes: 67 additions & 0 deletions scripts/Convert-ToWebM.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<#
.SYNOPSIS
Converts MP4 video files to WebM (VP9 codec) and extracts a WebP poster frame for site events.

.DESCRIPTION
Uses ffmpeg to create a web-optimized WebM file and a static WebP poster frame.
Outputs a PSCustomObject summarizing the conversion.

.PARAMETER Source
Path to the input MP4 file. Defaults to 'img/irrer_wissenschaftler.mp4'.

.EXAMPLE
./scripts/Convert-ToWebM.ps1 -Source "img/irrer_wissenschaftler.mp4"
#>

[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $false)]
[string]$Source = "img/irrer_wissenschaftler.mp4"
)

# Ensure ffmpeg is installed
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
Write-Error "ffmpeg is required but was not found in PATH."
return
}

# Resolve file paths
$ResolvedSource = Resolve-Path $Source -ErrorAction SilentlyContinue
if (-not $ResolvedSource -or -not (Test-Path $ResolvedSource.Path)) {
Write-Error "Source file '$Source' not found."
return
}

$SourceFile = $ResolvedSource.Path
$BaseDir = Split-Path $SourceFile -Parent
$BaseName = [System.IO.Path]::GetFileNameWithoutExtension($SourceFile)

$WebmFile = Join-Path $BaseDir "$BaseName.webm"
$PosterFile = Join-Path $BaseDir "${BaseName}_poster.webp"

Write-Host "🎥 Converting '$BaseName.mp4' to VP9 WebM..." -ForegroundColor Cyan

# 1. Convert to WebM (VP9, CRF 32, no audio for silent teasers)
$ffmpegWebmArgs = @("-y", "-i", "`"$SourceFile`"", "-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0", "-an", "`"$WebmFile`"")
Start-Process -FilePath "ffmpeg" -ArgumentList ($ffmpegWebmArgs -join " ") -NoNewWindow -Wait

# 2. Extract WebP Poster Frame
Write-Host "🖼️ Generating poster frame '$BaseName`_poster.webp'..." -ForegroundColor Cyan
$ffmpegPosterArgs = @("-y", "-i", "`"$SourceFile`"", "-vframes", "1", "`"$PosterFile`"")
Start-Process -FilePath "ffmpeg" -ArgumentList ($ffmpegPosterArgs -join " ") -NoNewWindow -Wait

# Gather file sizes
$OriginalSizeMB = [math]::Round((Get-Item $SourceFile).Length / 1MB, 2)
$WebmSizeMB = [math]::Round((Get-Item $WebmFile).Length / 1MB, 2)
$PosterSizeKB = [math]::Round((Get-Item $PosterFile).Length / 1KB, 2)

# Always use PSCustomObject for PowerShell output
[PSCustomObject]@{
SourceFile = $SourceFile
OriginalSizeMB = $OriginalSizeMB
WebmFile = $WebmFile
WebmSizeMB = $WebmSizeMB
PosterFile = $PosterFile
PosterSizeKB = $PosterSizeKB
SavingsPercent = [math]::Round(((1 - ($WebmSizeMB / $OriginalSizeMB)) * 100), 1)
}
2 changes: 1 addition & 1 deletion scripts/validate-images.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');

const imgDir = path.join(__dirname, '../img');
const allowedExtensions = ['.webp', '.svg', '.ico'];
const allowedExtensions = ['.webp', '.svg', '.ico', '.webm', '.mp4'];
// Allowed as fallback for older browsers
const exceptionFiles = ['hintergrund.jpg', 'psugh-logo.png'];
// Note: We already converted psugh-logo.png,
Expand Down
Loading