Developers
Video player & embed
The Bluvy Tube player fits on any website with a single iframe, just like YouTube or Vimeo. It plays Bluesky videos over HLS, with quality levels, captions, chapters and picture-in-picture.
Quick start
Copy this iframe. Its URL is built straight from the video post’s AT URI: the author’s did (or handle), then the post’s rkey.
<iframe
src="https://tube.bluvy.app/embed/at/did:plc:hqjanxqtecyflxbhpcgwmwkj/3mygkx5jim526"
width="560" height="315"
title="Bluvy Tube"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
Embed URLs
Embed URLs have no language prefix. They are stable, so you can store them.
| URL | Usage |
|---|---|
/embed/at/{did|handle}/{rkey} |
Recommended. Built from the post’s AT URI (at://did/app.bsky.feed.post/rkey), and accepts either a DID or a handle. If Bluvy Tube doesn’t know the video yet, it indexes it on the fly. |
/embed/video/{id} |
Bluvy Tube’s internal numeric id, the one in the /video/id page. This is the format the site’s “Embed” button produces. |
/embed/player?did=…&rkey=… |
Query-string variant of the first form, handy when the URL is built dynamically. |
/embed/player?stream=https://video.bsky.app/watch/{did}/{cid}/playlist.m3u8 |
From the HLS URL on Bluesky’s CDN. Only works for videos that are already indexed. |
For a Bluesky post, just split its AT URI:
// at://did:plc:abc123/app.bsky.feed.post/3kxyz → embed URL
function bluvyEmbedUrl(atUri) {
const [, repo, collection, rkey] = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/) ?? [];
if (collection !== 'app.bsky.feed.post') throw new Error('Not a post URI');
return `https://tube.bluvy.app/embed/at/${encodeURIComponent(repo)}/${encodeURIComponent(rkey)}`;
}
A 404 is returned if the video was deleted, hidden by moderation, or if its author opted out of Bluvy Tube. Plan a fallback, such as a link to the Bluesky post.
URL parameters
| Parameter | Values | Effect |
|---|---|---|
autoplay | 1, true | Starts playback on load. If the browser blocks autoplay with sound, the player retries muted. Add allow="autoplay" to the iframe. |
t | 90, 1m30s, 1h2m3s | Starts playback at this position, in seconds or as XhYmZs. Also works as a fragment: #t=90. |
lang / hl | fr, en | Language of the player interface. Defaults to the browser’s (Accept-Language), otherwise English. |
vertical / shorts | 1, true | Forces the vertical layout (blurred background, compact controls). Not needed for vertical videos, which are detected automatically. |
embedded | 1 | Reserved for the Bluvy Tube extension: turns autoplay off. Don’t rely on it. |
https://tube.bluvy.app/embed/at/did:plc:hqjanxqtecyflxbhpcgwmwkj/3mygkx5jim526?autoplay=1&t=1m30s&lang=fr
Sizing and responsiveness
The player fills the whole iframe and fits the video without cropping it. The site’s “Embed” button produces a 560×315 (16:9) iframe. For a responsive layout, give the iframe an aspect-ratio instead: 16:9 for a landscape video, 9:16 for a short.
<style>
.bluvy-embed { width: 100%; aspect-ratio: 16 / 9; border: 0; }
.bluvy-embed--vertical { max-width: 360px; aspect-ratio: 9 / 16; }
</style>
<iframe class="bluvy-embed"
src="https://tube.bluvy.app/embed/at/did:plc:hqjanxqtecyflxbhpcgwmwkj/3mygkx5jim526"
title="Bluvy Tube"
allow="autoplay; encrypted-media; picture-in-picture; web-share"
allowfullscreen></iframe>
Attributes the iframe needs:
– allow="autoplay" for autoplay and for BLUVY_PLAY;
– picture-in-picture for picture-in-picture;
– allowfullscreen for fullscreen.
postMessage API
The page embedding the player can control it and follow its state with window.postMessage. Each message is an object with a type field.
Commands (your page → player)
| Message | Effect |
|---|---|
{ type: 'BLUVY_PLAY' } | Starts playback. If the browser blocks it, the player retries muted. |
{ type: 'BLUVY_PLAY', muted: true } | Mutes the sound (or unmutes it with muted: false), then starts playback. |
{ type: 'BLUVY_PAUSE' } | Pauses playback. |
Events (player → your page)
| Message | Sent when |
|---|---|
{ type: 'BLUVY_EMBED_MOUNTED', videoId } | The player has loaded and is ready for commands. videoId is the internal numeric id. |
{ type: 'BLUVY_VIDEO_PLAYING' } | Playback starts, including when it resumes after a pause. |
{ type: 'BLUVY_VIDEO_PAUSED' } | Playback is paused. |
const BLUVY_ORIGIN = 'https://tube.bluvy.app';
const frame = document.querySelector('#bluvy-player');
// Register the listener before the iframe loads: BLUVY_EMBED_MOUNTED is sent right away.
window.addEventListener('message', (event) => {
// Only trust messages from the Bluvy player you embedded.
if (event.origin !== BLUVY_ORIGIN || event.source !== frame.contentWindow) return;
switch (event.data?.type) {
case 'BLUVY_EMBED_MOUNTED':
console.log('Player ready, video', event.data.videoId);
break;
case 'BLUVY_VIDEO_PLAYING':
console.log('Playing');
break;
case 'BLUVY_VIDEO_PAUSED':
console.log('Paused');
break;
}
});
// Commands: always target the Bluvy origin, never '*'.
frame.contentWindow.postMessage({ type: 'BLUVY_PLAY', muted: true }, BLUVY_ORIGIN);
frame.contentWindow.postMessage({ type: 'BLUVY_PAUSE' }, BLUVY_ORIGIN);
The player posts its events with a "*" target, so always check event.origin and event.source on your side. The BLUVY_SET_SYNC_TOKEN message is internal to the Bluvy Tube extension and is not meant for integrations.
oEmbed
Bluvy Tube is an oEmbed provider: given a link to a video, the endpoint returns the embed code and the video’s metadata. This is what lets WordPress, Discourse or Ghost turn a pasted link into a player.
GET https://tube.bluvy.app/oembed?url=https%3A%2F%2Ftube.bluvy.app%2Fen%2Fvideo%2F2579051&maxwidth=640
| Parameter | Effect |
|---|---|
url | Required. Any public address of a video on this site: a /fr|en/video/id page, an embed URL, an /at/… link, or an at://did/app.bsky.feed.post/rkey AT URI. |
maxwidth, maxheight | Optional. Maximum iframe size, keeping the aspect ratio. Defaults to 560×315, or 315×560 for a vertical video. |
format | Only json, the default, is available. xml returns a 501. |
Example response:
{
"type": "video",
"version": "1.0",
"title": "…",
"author_name": "Alice",
"author_url": "https://tube.bluvy.app/en/creator/alice.bsky.social",
"provider_name": "Bluvy Tube",
"provider_url": "https://tube.bluvy.app/",
"cache_age": 3600,
"thumbnail_url": "https://video.bsky.app/watch/did:plc:abc123/bafkrei…/thumbnail.jpg",
"thumbnail_width": 1920,
"thumbnail_height": 1080,
"width": 560,
"height": 315,
"html": "<iframe src=\"https://tube.bluvy.app/embed/video/2579051\" width=\"560\" height=\"315\" …></iframe>"
}
Possible errors:
– 400 if url is missing;
– 404 if the URL doesn’t point to a visible video on this site;
– 429 over the rate limit, with a Retry-After header.
Responses allow CORS (Access-Control-Allow-Origin: *) and can be cached for an hour.
Auto-discovery
Every video page declares its oEmbed endpoint in its <head>, so consumers that support auto-discovery need no setup:
<link rel="alternate" type="application/json+oembed"
href="https://tube.bluvy.app/oembed?url=https%3A%2F%2Ftube.bluvy.app%2Fen%2Fvideo%2F2579051" title="…">
Registering Bluvy Tube as a provider
Platforms that only accept registered providers need to add Bluvy Tube to their list. With WordPress, for example:
<?php
// WordPress — e.g. in a small plugin or your theme's functions.php
wp_oembed_add_provider('#https?://tube\.bluvy\.app/(fr|en)/video/\d+#i', 'https://tube.bluvy.app/oembed', true);
wp_oembed_add_provider('#https?://tube\.bluvy\.app/at/.+#i', 'https://tube.bluvy.app/oembed', true);
Player features
The player is built on Video.js v10 (web components) and hls.js. It offers the following features, in the embed and on the site:
- Quality: Auto (adaptive) or a fixed resolution, among those in the HLS stream.
- Speed: 0.5× to 2×. Loop playback can be turned on from the settings or the right-click menu.
- Captions in WebVTT, added by the creator, with one track per language.
- Chapters: segments on the progress bar, chapter title on hover, and a menu to jump between them.
- Picture-in-picture: a dedicated button, plus an automatic switch when the tab goes to the background during playback.
- Media Session: play, pause and seek from media keys and the lock screen.
- Right-click: watch on Bluvy Tube, copy the link (with or without the current time), copy the embed code, loop.
WebVTT captions and chapters
The tracks can also be fetched directly, for another player or for automated processing. A missing track returns a 404.
GET https://tube.bluvy.app/en/video/2579051/captions/fr.vtt
GET https://tube.bluvy.app/en/video/2579051/chapters.vtt
Keyboard shortcuts
| Key | Effect | Available in |
|---|---|---|
| Space / K | Play / pause | Embed and site |
| M | Mute / unmute | Embed and site |
| F | Fullscreen | Embed and site |
| I | Picture-in-picture | Embed and site |
| ← / → | Back / forward 5 s | Site video page |
| ↑ / ↓ | Volume | Site video page |
| T | Theater mode | Site video page |
Deep links
To offer “Open in Bluvy Tube” from your app, use the /at resolver. It takes an AT URI and redirects (302) to the right page. The t parameter works on video pages too.
https://tube.bluvy.app/at?uri=at://did:plc:hqjanxqtecyflxbhpcgwmwkj/app.bsky.feed.post/3mygkx5jim526
https://tube.bluvy.app/at/did:plc:hqjanxqtecyflxbhpcgwmwkj/app.bsky.feed.post/3mygkx5jim526
https://tube.bluvy.app/at/alice.bsky.social
https://tube.bluvy.app/en/video/2579051?t=2m30s
| Collection | Redirects to |
|---|---|
app.bsky.feed.post | The video page, indexed on the fly if needed |
app.bluvy.tube.video.metadata | The video page, indexed on the fly if needed |
app.bluvy.tube.playlist | The playlist page |
app.bsky.actor.profile / DID / handle | The creator’s channel |
place.stream.livestream | The live stream (Streamplace) |
The language of the target page follows ?lang=fr|en, or else the Accept-Language header. An unsupported collection returns a 404.
Page metadata
Video pages expose:
– OpenGraph and Twitter Card, for link previews;
– a canonical link and its hreflang alternates;
– oEmbed discovery;
– at-tags that tie the page to its AT records.
<meta property="og:type" content="video.other">
<meta property="og:image" content="https://video.bsky.app/watch/…/thumbnail.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="canonical" href="https://tube.bluvy.app/en/video/2579051">
<link rel="alternate" type="application/json+oembed" href="https://tube.bluvy.app/oembed?url=…">
<meta name="at:canonical" content="at://did:plc:hqjanxqtecyflxbhpcgwmwkj/app.bsky.feed.post/3mygkx5jim526">
<meta name="at:alternate" content="at://did:plc:hqjanxqtecyflxbhpcgwmwkj/app.bluvy.tube.video.metadata/…">
<meta name="at:author" content="at://did:plc:hqjanxqtecyflxbhpcgwmwkj">
<meta name="at:me" content="at://did:plc:dgqudubx2plnmynqu5wtsoan">
Headers, caching and privacy
/embed/…pages sendContent-Security-Policy: frame-ancestors *, so they can be embedded anywhere. Every other page refuses to be framed (X-Frame-Options: DENY).- Embeds are served with
Cache-Control: public, max-age=120, stale-while-revalidate=600. - Embeds are
noindex, nofollow: only the video page is indexed. - Embed views are counted server-side, with no tracker in your page. Bots, prefetches and visitors who send
Sec-GPC: 1(Global Privacy Control) are not counted.
Current limitations
- The postMessage API reports neither the playback position, nor the end of the video, nor the volume. It doesn’t accept a seek command either.
- There is no URL parameter to start muted, loop, or hide the controls.
- Video pages don’t expose
twitter:player,og:videoor a JSON-LDVideoObjectyet. - The numeric id (
/embed/video/id,videoId) is specific to this Bluvy Tube instance. For a durable link, start from the AT URI.
Is one of these limitations blocking you? Let us know.