Docs › Build your own player
Build your own player
Use any player you like: a plain <video> tag, hls.js, video.js, Shaka, or a native app. The links come from GET /videos/{id}, which needs your secret API key, so the flow is:
- Your server gets the video's links and hands them to the page (never the API key).
- The page plays
streamUrl(adaptive, Pro and Scale) orplaybackUrl(MP4, every plan), withposterPlaybackUrlas the poster. - Before the links expire (
playbackExpiresAt), the page asks your server again and swaps the source, keeping the viewer's position. "Refreshing" is just fetching the status again; there's no separate endpoint.
Cache on your server. Your key allows 5 requests per second, so don't call the API once per viewer: keep each video's links and fetch new ones only when less than about a fifth of their lifetime is left. Ask for a long lifetime (?linkTtl=86400) to cache longer.
// Your server (Node.js): GET /api/videos/:id/links → the player's links, cached per video
const cache = new Map();
app.get("/api/videos/:id/links", async (req, res) => {
const hit = cache.get(req.params.id), now = Date.now() / 1000;
if (hit && hit.playbackExpiresAt - now > 0.2 * hit.ttl) return res.json(hit);
const r = await fetch(`${API}/videos/${req.params.id}?linkTtl=86400`, { headers: { Authorization: `Bearer ${API_KEY}` } });
if (!r.ok) return res.status(r.status).end();
const s = await r.json();
const links = { playbackUrl: s.playbackUrl, streamUrl: s.streamUrl, posterUrl: s.posterPlaybackUrl,
captionsUrl: s.captions?.vttUrl, playbackExpiresAt: s.playbackExpiresAt, ttl: s.playbackExpiresAt - now };
cache.set(req.params.id, links);
res.json(links);
});
// Program.cs (ASP.NET Core 8+). The API key stays on your server: set VideoPiper__ApiKey (or appsettings.json).
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("videopiper", client =>
{
client.BaseAddress = new Uri("https://api.videopiper.dev/v1/");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", builder.Configuration["VideoPiper:ApiKey"]);
});
var app = builder.Build();
// The player's links (<video-piper links-url="/api/videos/ID/links">), cached per video.
var cache = new ConcurrentDictionary<string, (JsonElement Links, double ExpiresAt, double Ttl)>();
app.MapGet("/api/videos/{id}/links", async (string id, IHttpClientFactory http) =>
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (cache.TryGetValue(id, out var hit) && hit.ExpiresAt - now > 0.2 * hit.Ttl) return Results.Json(hit.Links);
using var response = await http.CreateClient("videopiper").GetAsync($"videos/{Uri.EscapeDataString(id)}?linkTtl=86400");
if (!response.IsSuccessStatusCode) return Results.StatusCode((int)response.StatusCode);
var s = await response.Content.ReadFromJsonAsync<JsonElement>();
string? Text(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
var expires = s.GetProperty("playbackExpiresAt").GetDouble();
var links = JsonSerializer.SerializeToElement(new
{
playbackUrl = Text(s, "playbackUrl"),
streamUrl = Text(s, "streamUrl"),
posterUrl = Text(s, "posterPlaybackUrl"),
captionsUrl = s.TryGetProperty("captions", out var c) ? Text(c, "vttUrl") : null,
playbackExpiresAt = expires,
});
cache[id] = (links, expires, expires - now);
return Results.Json(links);
});
app.Run();
<!-- The page: plays HLS where it can, else the MP4, and refreshes the links before they expire -->
<video id="player" controls playsinline crossorigin="anonymous"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
const video = document.getElementById("player");
let hls;
async function load(keepPosition) {
const links = await (await fetch("/api/videos/VIDEO_ID/links")).json();
const at = video.currentTime, playing = !video.paused;
video.poster = links.posterUrl;
if (links.streamUrl && window.Hls?.isSupported()) {
hls?.destroy(); hls = new Hls(); hls.loadSource(links.streamUrl); hls.attachMedia(video);
} else {
video.src = links.streamUrl && video.canPlayType("application/vnd.apple.mpegurl") ? links.streamUrl : links.playbackUrl;
}
if (keepPosition) video.addEventListener("loadedmetadata", () => { video.currentTime = at; if (playing) video.play(); }, { once: true });
// Refresh a few minutes before expiry (or halfway, for short links)
const left = links.playbackExpiresAt - Date.now() / 1000;
setTimeout(() => load(true), Math.max(60, left - 300, left / 2) * 1000);
}
load(false);
</script>
- Captions: add
<track kind="subtitles" src="…vttUrl…" srclang="en" default>(thecrossoriginattribute on the video lets the browser load it). HLS players pick up the stream's own subtitle track without this. - Seeking works for the link's whole lifetime: the MP4 is served with range requests.
- Expired links answer
403. If a viewer pauses for a long time, callload(true)again on a playback error. - Lock links to your sites with allowed domains (below), so nobody can reuse them elsewhere.
- Easier: use our
<video-piper>web component, which does all of this for you, or embed a video with no code at all.