✈️ FlightPlan Developer Guide

← Back to Guide

Video & Streaming

FlightPlan's Video element wraps the HTML5 <video> tag with a fluent API, and the server streams the file with HTTP range requests so playback starts instantly and scrubbing is responsive. This page covers displaying and sizing a video, the preload (eager vs lazy) choice, and the /media/ serving plus range-streaming conventions that make it work.

Where Video Files Live

Video is served from the /media/ static route — a sibling of /images/, /css/, /js/, and /fonts/. The path after /media/ maps directly onto disk, relative to the running host's base directory, and each app's Media/ folder merges into one deployed Media/ folder at runtime.

// URL the browser requests:
/media/2026_Training.mp4

// File location on disk:
{appBaseDirectory}/Media/2026_Training.mp4

// Store the file in a Media\ folder at the root of FlightPlan.{project},
// and set Copy to Output Directory: Copy if newer  (csproj <None Update>).

Why a dedicated /media/ route instead of reusing an existing folder like /vault/? Static files are served WITHOUT the page's role check. A dedicated Media/ folder holds only files meant to be public (the video), so it can't accidentally expose other documents that happen to share a folder.

Supported formats: .mp4 (video/mp4), .webm (video/webm), .ogg (video/ogg). mp4 (H.264) is the safe default — every browser plays it.

Video Files Are Not in Git

Video binaries are large and bloat the repo and its history, so they are gitignored (*.mp4, *.webm, *.mov) and source-controlled separately. The csproj still references the file via <None Update> so it deploys locally when present, but a fresh clone will not have it until you fetch it from the separate media store.

Displaying a Video

The constructor takes the src URL. WithControls() adds the browser's play/scrub/volume controls — almost always what you want. Unlike Img, <video> is not a void element, so the Video element renders a real closing </video> tag (a self-closing <video/> is invalid HTML and silently misbehaves).

content.Add(new Video("/media/2026_Training.mp4")
    .WithControls());
Renders as:

Sizing and Responsive Width

WithWidth and WithHeight go through the inline Style, so any CSS length works — "640px", "100%", "50vw". For a video that fills its column but never grows past a maximum, combine width:100% with a max-width via WithStyle:

// Fixed size
new Video("/media/clip.mp4").WithControls().WithWidth("640px")

// Responsive, capped at 1200px (fills the column, never larger)
new Video("/media/clip.mp4")
    .WithControls()
    .WithStyle(s => s.Width("100%").Set("max-width", "1200px"))
Renders as:

The video keeps its intrinsic aspect ratio, so setting only the width scales the height automatically — same as Img.

Preload: Eager vs Lazy Loading

Preload is the video analog of the Img element's lazy loading. Where Img uses loading="lazy" to defer offscreen image downloads, Video uses the preload attribute to control how much it fetches before the user presses play:

new Video("/media/clip.mp4").WithControls().WithPreload("none")
new Video("/media/clip.mp4").WithControls().WithPreload("metadata")  // default
new Video("/media/clip.mp4").WithControls().WithPreload("auto")

Note the symmetry: both lazy mechanisms are native, client-side, and JavaScript-free — loading="lazy" for images, preload for video. Neither needs server support; they just change when the browser asks for bytes.

How Streaming Works (HTTP Range Requests)

When a <video> loads, the browser does not download the whole file up front. It sends a Range request asking for a byte window, and the server replies with 206 Partial Content for just that slice. This is what makes playback start immediately and lets you scrub to any point without waiting for a full download.

// Browser asks for a window:
GET /media/clip.mp4
Range: bytes=0-

// Server replies with a slice, not the whole file:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 0-1048575/58076826
Content-Length: 1048576
... (1 MB of video) ...

The FlightPlan server caps each slice at 1 MB. That is deliberate: browsers open a video with "bytes=0-" (everything from the start), and if the server honored that literally it would buffer the entire file into memory for one request. Capping the slice keeps per-request memory at ~1 MB no matter how large the video is — the browser simply requests the next window as it plays. Seeking works because a scrub sends a new Range starting at the seek point.

A request with no Range header still works — the server returns the whole file with 200 plus an Accept-Ranges: bytes header, so the player knows it MAY switch to range requests for seeking. An unsatisfiable range returns 416.

None of this requires page code — it is automatic for any file served from /media/ (or any static route). You just point the Video element at the URL.

Quick Reference

All fluent setters on Video:

← Back to Guide

Please wait...