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());
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"))
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:
- "none" — fetch nothing until play is pressed. The lightest option; use for videos far down a long page or rarely watched.
- "metadata" (the Video element's default) — fetch just enough to know duration and dimensions, so the player shows the right size and a real timeline without downloading the video. Best general-purpose choice.
- "auto" — the browser may eagerly buffer the whole video. Use only when you're confident the user will watch and want zero startup delay.
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:
- .WithSrc(string) — change the source URL after construction
- .WithControls(bool = true) — show the browser's playback controls
- .WithWidth(string) / .WithHeight(string) — sizing in any CSS length (via inline style)
- .WithPreload(string) — "none" | "metadata" (default) | "auto"
- .WithClass(string) — CSS class name
- .WithId(string) — element id, for CSS or JavaScript targeting
- .WithStyle(s => ...) — inline CSS via the Style fluent API (max-width, etc.)