Adaptive Video
Use Frontbacked adaptive video for video file fields stored in posts. Theme authors keep video files as normal post fields, and Frontbacked gives the frontend a ready-to-use player API with adaptive quality, posters, timeline thumbnails, progress, and playlist controls.
Basic Player
Use f-video with a Frontbacked video file object:
<video
f="true"
f-video="$.video"
f-video-player="standard"
f-video-quality-selector="true"
f-video-timeline-thumbnails="true"
playsinline
preload="metadata"
></video>
f-video accepts either the file object stored in the post or a plain video URL. For Frontbacked video files, the runtime uses adaptive playback when the video is ready and falls back to MP4 when adaptive playback is unavailable.
Processing Status
Video files are uploaded like any other file. When the file is read back, Frontbacked adds a runtime media object:
{
url: "uploads/...",
size: 18492320,
mimeType: "video/mp4",
fileName: "intro.mp4",
uploadTime: "2026-07-20T12:00:00.000Z",
upload: {
status: "completed",
progress: 100
},
media: {
id: "media_id",
type: "video",
status: "processing",
progress: 42,
poster: null,
playback: "/_fb/video/playback/media_id",
manifest: null,
fallback: null,
thumbnails: null,
duration: null,
width: null,
height: null,
renditions: [],
error: null
}
}
media.status can be:
| Status | Meaning |
|---|---|
uploaded | The file has been received and is waiting for preparation. |
queued | The video is waiting to be processed. |
processing | Renditions, poster, fallback, and thumbnails are being prepared. |
ready | The video can be played. |
failed | The video could not be prepared. Check media.error. |
Use media.progress for processing progress and upload.progress for upload progress.
JavaScript Attach
Frontbacked.video.attach(element, source, options) attaches adaptive playback and returns a player instance.
const player = await Frontbacked.video.attach(
document.querySelector("#heroVideo"),
post.video,
{
player: "standard",
qualitySelector: true,
timelineThumbnails: true,
layout: "landscape",
poster: "auto",
onPlayback(event, player) {
console.log(event.currentTime, event.remaining);
}
}
);
player.play();
The source can be:
post.video
post.video.media
"/videos/intro.mp4"
Use the post file object when you have it. It gives the runtime the most metadata.
Player Options
| Option | Values | Purpose |
|---|---|---|
player | "standard", "frontbacked", false | Enables the Frontbacked player UI. Use false for native browser controls. |
qualitySelector | true, false | Shows Auto, 240p, 360p, 480p, 720p, and other available renditions. |
timelineThumbnails | true, false | Shows storyboard thumbnails when the user hovers or scrubs the timeline. |
layout | "landscape", "portrait" | Uses a wide player or a short-style vertical player. |
poster | "auto" or URL | Uses the generated poster or a custom poster. |
autoplay | true, false | Requests autoplay. Browser autoplay rules still apply. |
muted | true, false | Starts muted or unmuted. |
hideControls | true, false | Hides every Frontbacked player control, including the timeline. |
hiddenControls | string array | Hides only selected controls. |
maxQuality | "auto" or height | Caps automatic quality selection. |
dataSaver | "respect" | Respects Save-Data and constrained-network hints. |
onPlayback | function | Receives time updates while the video plays. |
onTimeUpdate | function | Alias-style callback for playback time updates. |
events | object | Use events.playback or events.timeupdate callbacks. |
The matching HTML attributes use f-video-:
<video
f="true"
f-video="$.video"
f-video-player="standard"
f-video-layout="portrait"
f-video-hide-controls="false"
f-video-hidden-controls="time mute"
f-video-quality-selector="true"
f-video-timeline-thumbnails="true"
></video>
Hiding Controls
Hide everything:
await Frontbacked.video.attach(video, post.video, {
player: "standard",
hideControls: true
});
Hide only selected controls:
await Frontbacked.video.attach(video, post.video, {
player: "standard",
hiddenControls: ["mute", "time", "quality"]
});
Control names:
| Name | Hides |
|---|---|
play | Play and pause button. |
prev | Previous playlist button. |
next | Next playlist button. |
mute | Mute button and volume slider. |
time | Current time and duration pill. |
timeline | Seek bar, buffered bar, and thumbnail preview. |
quality | Quality selector. |
fullscreen | Fullscreen button. |
Aliases such as pause, volume, seekbar, timebar, previous, and full are normalized to the matching control names.
Programmatic Control
The object returned from Frontbacked.video.attach() controls the player directly:
player.play();
player.pause();
player.toggle();
player.seekTo(60);
player.seekBy(10);
player.seekBy(-10);
player.mute();
player.unmute();
player.setMuted(true);
player.setVolume(0.4);
player.setQuality("auto");
player.setQuality(480);
player.setLayout("portrait");
await player.next();
await player.prev();
player.destroy();
Read playback state:
player.currentTime();
player.duration();
player.remaining();
player.list();
Keyboard shortcuts work when the player is active:
| Key | Action |
|---|---|
Space or K | Play or pause. |
J | Rewind 10 seconds. |
L | Forward 10 seconds. |
Left Arrow | Rewind 5 seconds. |
Right Arrow | Forward 5 seconds. |
M | Mute or unmute. |
F | Toggle fullscreen. |
Clicking the video surface toggles play and pause. Double-clicking the left side rewinds, and double-clicking the right side forwards.
Playback Callback
Use onPlayback when your theme needs to react to the current time or remaining time.
const player = await Frontbacked.video.attach(video, post.video, {
player: "standard",
onPlayback(event, player) {
if (event.remaining < 10) {
player.appendVideo(nextVideo);
}
Frontbacked.setState("videoProgress", {
reached: event.reached,
remaining: event.remaining,
percent: event.percent
});
}
});
Callback data:
{
video,
player,
currentTime: 51.2,
reached: 51.2,
remaining: 128.8,
duration: 180,
percent: 28.4
}
Playlist and Append
Every attached player starts with a one-item playlist: the video passed to Frontbacked.video.attach(). appendVideo() adds more videos after the current list, so the player can move from one video to the next without rebuilding your own player UI.
Append one video:
player.appendVideo(nextVideo);
Append many videos:
player.appendVideo([secondVideo, thirdVideo]);
appendVideo() accepts the same source shapes as Frontbacked.video.attach(): a stored file object, a media object, or a URL. Calling it multiple times keeps adding to the existing list.
player.appendVideo(featuredPost.video);
player.appendVideo(morePosts.map((post) => post.video));
Move through the playlist from JavaScript:
await player.next();
await player.prev();
When appended videos exist, the standard player shows next and prev controls unless those controls are hidden. The next control appears when there is another retained video after the current one. The prev control appears when there is a retained video before the current one, or when older videos were evicted and your theme has provided a previous getter.
When a playlist move happens, the same video element is reused, playback starts from 0, and identical video sources are still treated as separate playlist entries. That means appending the same video twice creates two playable entries, while browser caching can still make the second load fast.
The next video is prepared near the end of the current video so playback can continue smoothly.
The player keeps up to 50 videos in its list. When more are appended, the oldest entries are evicted first. If your theme can fetch older entries, provide a previous getter:
player.setPrevGetter(async ({ evictedCount, firstAvailable, player }) => {
return await loadPreviousVideoBefore(firstAvailable);
});
setPrevGetter(callback) is called only when all of these are true:
- The user or theme calls
player.prev(), or the user clicks theprevcontrol. - The current playlist item is the first retained item in the local list.
- One or more older items were evicted because the playlist passed 50 items.
- A previous getter callback has been set.
If no previous getter is set, the prev control is hidden when the current video is the first retained item. In that case, the player does not try to fetch older videos; it simply stays on the first available playlist item.
The callback receives:
| Argument | Meaning |
|---|---|
evictedCount | Number of older playlist entries that were removed from the front of the local list. |
firstAvailable | Source object for the first video still retained in the playlist. Use it as a cursor when fetching older videos. |
player | The player instance, useful if your callback wants to inspect current state or pause while loading. |
Return one video, an array of videos, or nothing:
player.setPrevGetter(async ({ evictedCount, firstAvailable }) => {
const previousPosts = await fetchOlderVideoPosts({
beforeId: firstAvailable?.$id || firstAvailable?.id,
limit: Math.min(evictedCount, 10)
});
return previousPosts.map((post) => post.video);
});
When the callback returns videos, Frontbacked inserts them before the first retained item, reduces evictedCount by the number returned, and plays the newest returned item. Returning an empty array, null, or undefined leaves the playlist unchanged.
Use player.list() when your theme needs the currently retained playlist sources:
const retainedVideos = player.list();
player.list() only returns the retained items, not entries that have already been evicted.
Quality Selection
When qualitySelector is enabled, the player shows Auto plus every available rendition. In Auto, Frontbacked considers the player size, device pixel ratio, available renditions, browser network hints, Save-Data, buffer health, and playback telemetry.
Manual quality selection pins playback to the selected rendition until the user returns to Auto:
player.setQuality(360);
player.setQuality("auto");
Only renditions that exist for that video are shown. For example, a 480p source can expose 240p, 360p, and 480p, while a 1080p source can expose higher renditions when available.
Portrait Player
Use portrait layout for short-style videos:
await Frontbacked.video.attach(video, post.video, {
player: "standard",
layout: "portrait",
qualitySelector: true,
timelineThumbnails: true
});
Or with HTML:
<video
f="true"
f-video="$.video"
f-video-player="standard"
f-video-layout="portrait"
f-video-quality-selector="true"
f-video-timeline-thumbnails="true"
></video>
Portrait mode uses a 9:16 player, portrait fullscreen framing, top controls, and portrait-style timeline previews.
Events
Listen for player events when you need lower-level hooks:
window.addEventListener("frontbacked:video-ready", (event) => {
console.log(event.detail.playback);
});
window.addEventListener("frontbacked:video-quality", (event) => {
console.log(event.detail.height);
});
window.addEventListener("frontbacked:video-error", (event) => {
console.log(event.detail);
});
Common events:
| Event | Purpose |
|---|---|
frontbacked:video-status | Processing status for a video that is not ready yet. |
frontbacked:video-ready | Playback sources were attached. |
frontbacked:video-quality | The active rendition changed. |
frontbacked:video-playback | Time update with currentTime, remaining, and percent. |
frontbacked:video-error | Playback or source loading error. |
Uploading Videos
Videos are uploaded with the normal file upload flow:
await Frontbacked.uploadPost({
type: "videos",
post: {
title: "Launch demo",
video: document.querySelector("#video").files[0]
},
onUploadProgress(progress) {
console.log(progress.percent);
}
});
Use file.upload.progress for upload progress and file.media.progress for processing progress after the post is read back.
if (post.video.media.status === "ready") {
await Frontbacked.video.attach(video, post.video, { player: "standard" });
}
See Upload File for upload limits, progress events, and resume.