Why a plain video file is not enough
Putting an MP4 behind a <video> tag works for a short clip on a fast connection. For anything longer it means every viewer downloads the same large file whatever their bandwidth, and seeking depends on how the file was encoded.
HLS fixes this by splitting a video into short segments at several quality levels, plus a manifest that lists them. The player picks a quality that suits the connection and switches as it changes. The catch is producing those renditions: normally an ffmpeg job, a queue, somewhere to run it, and a way to know when it has finished.
What Fileboy does for you
You upload the original file. Fileboy encodes it into the renditions, generates a poster image, and serves a manifest. HLS is available, and DASH is too. When you ask for an auto manifest it chooses between them.
Upload from the Videos page of your app dashboard. Processing is not instant, and a video that is still encoding reports that status rather than failing, so a player shows a friendly message until it is ready.
Video counts against your plan's video limit, separately from files. The free plan includes three videos, which is enough to try this end to end. See pricing.
Play it on your page
Each video has an ID, shown in the dashboard. Load the player script once and give <fileboy-player> the ID. The player loads the stream, shows the poster until the viewer presses play, and offers quality, speed and captions menus.
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@cd2/fileboy-player@1.2.0/dist/fileboy-player.js"
></script>
<fileboy-player video-id="YOUR_VIDEO_ID"></fileboy-player>Useful attributes: autoplay, loop, preload (set up the stream before the first click) and poster to override the poster. The element fires play, pause, timeupdate and ended events with { currentTime, duration } in event.detail, for progress tracking.
Play it in React
npm install @cd2/fileboy-playerThe player is a web component, so React renders it like any other element. Import the package once to register it.
import { useEffect, useRef } from "react"
import "@cd2/fileboy-player"
export function Lesson({ videoId }: { videoId: string }) {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
const player = ref.current
const onEnded = () => console.log("finished")
player?.addEventListener("ended", onEnded)
return () => player?.removeEventListener("ended", onEnded)
}, [])
return <fileboy-player ref={ref} video-id={videoId} />
}On React 16 to 18, pass boolean attributes as an empty string (preload="") and listen for events with a ref, as above. React 19 passes both through directly.
Or use an iframe
On a page where you cannot add scripts, paste an iframe. The dashboard gives you the embed code on each video's page, and it looks like this:
<iframe
src="https://fileboy.io/video/embed/YOUR_VIDEO_ID"
width="560"
height="315"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
></iframe>Upload from your server
Uploads from a script or backend use the same API. The Ruby, Python and PHP libraries wrap it, and the HTTP call is shown too. The shared demo key cannot upload video.
// Video upload is part of the HTTP API. The SDKs for Ruby, Python and PHP wrap it:
// see the Ruby, Python or PHP tab, or upload from the dashboard.video = fileboy.upload_video("./clips/launch.mp4", name: "Launch day")
# encoding runs in the background: poll until it is ready
data = fileboy.fetch_video_data(video.id)
data["extractionStatus"]
fileboy.build_video_manifest_url(video.id, type: :hls)
fileboy.build_video_poster_url(video.id)video = client.upload_video("./clips/launch.mp4", name="Launch day")
# encoding runs in the background: poll until it is ready
data = client.fetch_video_data(video.id)
data["extractionStatus"]
client.build_video_manifest_url(video.id, type="hls")
client.build_video_poster_url(video.id)$video = $fileboy->uploadVideo('./clips/launch.mp4', name: 'Launch day');
// encoding runs in the background: poll until it is ready
$data = $fileboy->fetchVideoData($video->id);
$data['extractionStatus'];
$fileboy->buildVideoManifestUrl($video->id, type: 'hls');
$fileboy->buildVideoPosterUrl($video->id);curl https://api.fileboy.io/videos/upload \
-H "Authorization: public YOUR_PUBLIC_KEY" \
-F "file=@./clips/launch.mp4;type=video/mp4" \
-F 'data={"name":"Launch day"}'Transcripts attach to a video as WebVTT or plain text, one per language:
id = fileboy.upload_video_transcript(video.id, "./clips/launch.vtt", language: "English")
text = fileboy.fetch_video_transcript(video.id, id)transcript_id = client.upload_video_transcript(video.id, "./clips/launch.vtt", language="English")
text = client.fetch_video_transcript(video.id, transcript_id)$id = $fileboy->uploadVideoTranscript($video->id, './clips/launch.vtt', language: 'English');
$text = $fileboy->fetchVideoTranscript($video->id, $id);curl https://api.fileboy.io/videos/VIDEO_ID/transcripts/upload \
-H "Authorization: public YOUR_PUBLIC_KEY" \
-F "file=@./launch.vtt;type=text/vtt" \
-F 'data={"language":"English"}'Use your own player instead
The manifest is a normal URL, so Safari can play it natively and other browsers can use a library such as hls.js or Shaka Player. Ask for the HLS manifest explicitly with type=hls.
https://api.fileboy.io/videos/YOUR_VIDEO_ID/manifest?type=hls
# a poster image for the video
https://api.fileboy.io/videos/YOUR_VIDEO_ID/posterWhere this stops
This is streaming of video you upload. It is not live streaming, and it does not do per-viewer rendering, in-video editing or a content moderation pipeline. If video is the centre of your product, compare the options before committing: see Fileboy vs Cloudinary for an honest account of where a larger platform is the better call.
Checked against the current libraries in November 2026. Found something that no longer works? Tell us.