The central asset repository for high-performance HLS (HTTP Live Streaming) and MP4 video delivery across the Franc Vila and Badreya web platforms.
- π Overview
- β‘ Why HLS (HTTP Live Streaming)?
- π Repository Structure
- βοΈ Prerequisites
- π How to Convert Videos
- π οΈ How the Conversion Script Works
- π URL Structure & Hosting
- π» React & Next.js Integration
- π Developer Workflow
This repository acts as the media hosting and processing backend for Badreya and Franc Vila web platforms. Instead of serving massive static .mp4 files directlyβwhich delays page rendering, causes buffering, and consumes excessive mobile dataβvideos are processed into HLS (HTTP Live Streaming) directories. This allows the websites to serve fast, modern, and adaptive video streams tailored directly to each user's network connection.
- Adaptive Bitrate Streaming: The player automatically shifts between 1080p (high fidelity) and 480p (lighter weight) depending on the user's internet speed, ensuring zero buffer lags.
- Instant Playback (Fast Start): The video is split into small chunks (6 seconds each). The browser only needs to download the first segment to start playing, resulting in an instant-on user experience.
- Broad Native Compatibility: Works natively on Safari and iOS devices, and seamlessly on Chrome, Firefox, and Edge via standard integration libraries (such as
hls.js).
video-v2/
βββ sources/ # Raw MP4 masters (git-ignored, see .gitignore)
β βββ fin3.mp4
β βββ don-blue.mp4
βββ scripts/
β βββ convert_hls.sh # π οΈ HLS conversion script (1080p/720p/480p ladder + poster)
β βββ validate_hls.sh # QA gate: segments, playlists, posters, ABR alignment
β βββ cleanup.sh # Finds stray/orphaned video files (dry-run by default)
βββ tools/
β βββ test-player.html # hls.js QA harness: paste a master.m3u8 URL to test
βββ deploy/
β βββ nginx.hls.conf.example # MIME types + cache headers if self-hosting/CDN origin
β βββ _headers # Same, for Cloudflare Pages / Netlify
βββ .github/workflows/transcode.yml # Manual CI transcode (stopgap, see file header)
βββ hls/ # HLS output ONLY β nothing else belongs here
βββ fin3/
β βββ master.m3u8
β βββ poster.jpg
β βββ 720p/{index.m3u8, segment_000.ts, ...}
β βββ 480p/{index.m3u8, segment_000.ts, ...}
βββ ...
See docs/hls-migration-review.md (or the shared artifact) for the full architecture review this structure is based on.
To run the local conversion script on your machine, you need FFmpeg and FFprobe installed:
# Install FFmpeg using Homebrew on macOS
brew install ffmpegVerify that the utilities are installed correctly by running:
ffmpeg -version
ffprobe -versionFollow these steps to convert any standard .mp4 video file into an HLS adaptive streaming bundle:
Put the raw MP4 in sources/ (this folder is git-ignored β see .gitignore).
./scripts/convert_hls.sh sources/<input_file> <slug> [--presets=1080p,720p,480p] [--force]- Convert
fin3.mp4with the default 1080p/720p/480p ladder:./scripts/convert_hls.sh sources/fin3.mp4 fin3
- Only generate 720p and 480p (e.g. a video that's already small):
./scripts/convert_hls.sh sources/ring.MP4 ring --presets=720p,480p
- Re-run and overwrite an existing hls/ without the confirmation prompt:
./scripts/convert_hls.sh sources/fin3.mp4 fin3 --force
The script skips any rung above the source's native resolution (never upscales), forces
keyframes at exact 6-second boundaries so every rendition cuts identically, writes a
poster.jpg thumbnail alongside master.m3u8, and self-validates its own output
via scripts/validate_hls.sh before exiting β if validation fails, the script exits
non-zero and does not report success.
./scripts/cleanup.sh # dry run β lists problems
./scripts/cleanup.sh --force # moves stray hls/*.mp4 files into sources/python3 -m http.server 8000
open tools/test-player.html # paste http://localhost:8000/hls/<slug>/master.m3u8The convert_hls.sh script automates the transcoding pipeline:
- Resolution & Bitrate Ladder: Encodes up to four capped-CRF renditions β
1080p(~5000kbps),720p(~2800kbps),480p(~1400kbps),360p(~800kbps) β skipping any rung above the source's native resolution. - Chunk Segmentation: Cuts every rendition into independent 6-second segments (
segment_NNN.ts), with keyframes forced at exact segment boundaries so every rendition cuts at identical timestamps (required for clean ABR switching). - Thumbnail: Extracts a
poster.jpgat ~10% into the video. - Master Playlist Generation (
master.m3u8): Computes bandwidth/resolution per rung from the source's probed dimensions, reads the actual encoded profile/level back fromffprobeto build an accurateCODECSstring per rung (no hardcoded guess), and writes a standards-compliant multi-variant playlist with#EXT-X-INDEPENDENT-SEGMENTS. - Self-Validation: Runs
scripts/validate_hls.shagainst its own output before exiting β checks every playlist, every segment's existence and decodability, poster validity, and that all renditions of a video have matching segment counts (the precondition for clean ABR switching). A failed validation makes the script exit non-zero, so a bad encode never gets treated as done, locally or in CI.
This repository is hosted on GitHub Pages, serving files directly as a static content delivery network (CDN):
- GitHub Repository:
https://github.com/simaa99/video-v2 - Static Content Base URL:
https://simaa99.github.io/video-v2/
To play or stream any converted video in your code, target the folder's master.m3u8 playlist using this structure:
https://simaa99.github.io/video-v2/hls/<folder_name>/master.m3u8
Folder names match the slugs under
hls/(see Repository Structure).
To play adaptive HLS streams smoothly in your web apps, we recommend implementing a custom video component utilizing hls.js for non-native browsers, and native fallback for Apple devices.
Here is a ready-to-use, optimized React component:
"use client";
import { useEffect, useRef } from "react";
import Hls from "hls.js";
interface HlsVideoPlayerProps {
src: string; // Path to the master.m3u8 file
poster?: string; // Image shown before the video starts
className?: string;
autoPlay?: boolean;
muted?: boolean;
loop?: boolean;
onFatalError?: (message: string) => void; // e.g. show a fallback UI
}
const MAX_ERROR_RETRIES = 3;
export default function HlsVideoPlayer({
src,
poster,
className = "",
autoPlay = true,
muted = true,
loop = true,
onFatalError,
}: HlsVideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
let hls: Hls | null = null;
let retryCount = 0;
const play = () => {
if (autoPlay) video.play().catch((err) => console.log("Autoplay was blocked:", err));
};
if (Hls.isSupported()) {
// For browsers without native HLS support (Chrome, Firefox, Edge, etc.)
hls = new Hls({
maxMaxBufferLength: 30, // Buffers up to 30 seconds ahead for fluid playback
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
retryCount = 0; // reset once a manifest has loaded successfully
play();
});
// Non-fatal errors (a dropped segment fetch, a buffer stall) are
// recovered from internally by hls.js and don't need our intervention.
// Fatal errors need an explicit recovery strategy or the player is
// left permanently frozen/black with no feedback to the user.
hls.on(Hls.Events.ERROR, (_event, data) => {
if (!data.fatal) return;
if (retryCount >= MAX_ERROR_RETRIES) {
console.error(`HLS fatal error, giving up after ${MAX_ERROR_RETRIES} retries:`, data.type, data.details);
hls?.destroy();
onFatalError?.(`Playback failed after ${MAX_ERROR_RETRIES} retries: ${data.details}`);
return;
}
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
retryCount += 1;
console.warn(`HLS network error (attempt ${retryCount}/${MAX_ERROR_RETRIES}), retrying:`, data.details);
// Backs off slightly on each retry instead of hammering a segment
// that's 404ing because a re-encode is still mid-flight.
setTimeout(() => hls?.startLoad(), 1000 * retryCount);
break;
case Hls.ErrorTypes.MEDIA_ERROR:
retryCount += 1;
console.warn(`HLS media error (attempt ${retryCount}/${MAX_ERROR_RETRIES}), attempting recovery:`, data.details);
hls?.recoverMediaError();
break;
default:
console.error("Unrecoverable HLS error, destroying instance:", data.type, data.details);
hls?.destroy();
onFatalError?.(`Unrecoverable playback error: ${data.details}`);
break;
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
// For browsers with native HLS support (Safari on macOS, iOS devices)
video.src = src;
video.addEventListener("loadedmetadata", play);
// Safari's native player surfaces fatal errors via the <video> element
// itself rather than hls.js -- there's no retry primitive to call here,
// but we still want visibility instead of a silently frozen player.
video.addEventListener("error", () => {
const err = video.error;
console.error("Native HLS playback error:", err?.code, err?.message);
onFatalError?.(`Native playback error (code ${err?.code ?? "unknown"})`);
});
}
return () => {
// Prevent memory leaks by destroying the HLS instance on component unmount
if (hls) {
hls.destroy();
}
};
}, [src, autoPlay, onFatalError]);
return (
<video
ref={videoRef}
poster={poster}
className={`w-full h-full object-cover ${className}`}
muted={muted}
loop={loop}
playsInline
controls
/>
);
}Error handling notes:
- Network errors (e.g. a segment 404s because a re-encode is mid-flight) retry with a short backoff, up to
MAX_ERROR_RETRIEStimes. - Media errors (decode issues) call
hls.recoverMediaError(), hls.js's built-in recovery path. - Other fatal errors, and any error type after retries are exhausted, destroy the
hls.jsinstance and call the optionalonFatalErrorcallback so the host page can show a fallback UI instead of a silently frozen or black<video>element. - Safari's native path has no
hls.jsretry primitive to call, but still surfaces failures via the<video>element's ownerrorevent so they're not silent there either.
Use this checklist when introducing a new video asset to the system:
- 1. Transfer Video: Place the original file in
sources/(git-ignored β never commit raw masters, see.gitignore). - 2. Convert Video: From the repo root:
./scripts/convert_hls.sh sources/my-video.mp4 my-video-folder
- 3. Inspect Outputs:
convert_hls.shalready ranscripts/validate_hls.shautomatically (step 2 fails if this doesn't pass), but re-run it manually any time:./scripts/validate_hls.sh my-video-folder(or./scripts/validate_hls.sh --allto health-check every video inhls/at once). Sanity-check playback intools/test-player.htmltoo. - 4. Clean up: Run
./scripts/cleanup.shto confirm no stray files were left behind. - 5. Push to Git: Add, commit, and push only the generated
hls/folder (neversources/):git add hls/my-video-folder git commit -m "feat: add HLS stream for my-video" git push origin main - 6. Wait & Verify: Give GitHub Pages about a minute to complete deployment, then check the livestream URL directly in a player or browser:
https://simaa99.github.io/video-v2/hls/my-video-folder/master.m3u8 - 7. Integrate: Embed the new HLS URL into your target codebase using the
HlsVideoPlayercomponent below.
Designed with precision to provide the ultimate media streaming experience.