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:

StatusMeaning
uploadedThe file has been received and is waiting for preparation.
queuedThe video is waiting to be processed.
processingRenditions, poster, fallback, and thumbnails are being prepared.
readyThe video can be played.
failedThe 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

OptionValuesPurpose
player"standard", "frontbacked", falseEnables the Frontbacked player UI. Use false for native browser controls.
qualitySelectortrue, falseShows Auto, 240p, 360p, 480p, 720p, and other available renditions.
timelineThumbnailstrue, falseShows 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 URLUses the generated poster or a custom poster.
autoplaytrue, falseRequests autoplay. Browser autoplay rules still apply.
mutedtrue, falseStarts muted or unmuted.
hideControlstrue, falseHides every Frontbacked player control, including the timeline.
hiddenControlsstring arrayHides only selected controls.
maxQuality"auto" or heightCaps automatic quality selection.
dataSaver"respect"Respects Save-Data and constrained-network hints.
onPlaybackfunctionReceives time updates while the video plays.
onTimeUpdatefunctionAlias-style callback for playback time updates.
eventsobjectUse 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:

NameHides
playPlay and pause button.
prevPrevious playlist button.
nextNext playlist button.
muteMute button and volume slider.
timeCurrent time and duration pill.
timelineSeek bar, buffered bar, and thumbnail preview.
qualityQuality selector.
fullscreenFullscreen 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:

KeyAction
Space or KPlay or pause.
JRewind 10 seconds.
LForward 10 seconds.
Left ArrowRewind 5 seconds.
Right ArrowForward 5 seconds.
MMute or unmute.
FToggle 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:

  1. The user or theme calls player.prev(), or the user clicks the prev control.
  2. The current playlist item is the first retained item in the local list.
  3. One or more older items were evicted because the playlist passed 50 items.
  4. 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:

ArgumentMeaning
evictedCountNumber of older playlist entries that were removed from the front of the local list.
firstAvailableSource object for the first video still retained in the playlist. Use it as a cursor when fetching older videos.
playerThe 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:

EventPurpose
frontbacked:video-statusProcessing status for a video that is not ready yet.
frontbacked:video-readyPlayback sources were attached.
frontbacked:video-qualityThe active rendition changed.
frontbacked:video-playbackTime update with currentTime, remaining, and percent.
frontbacked:video-errorPlayback 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.