Skip to main content
Bluvy Tube

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.

html
<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>

Try the postMessage API

Messages sent to the player (→) and received from it (←) are listed below.

    Embed URLs

    Embed URLs have no language prefix. They are stable, so you can store them.

    URLUsage
    /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:

    js
    // 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

    ParameterValuesEffect
    autoplay1, trueStarts playback on load. If the browser blocks autoplay with sound, the player retries muted. Add allow="autoplay" to the iframe.
    t90, 1m30s, 1h2m3sStarts playback at this position, in seconds or as XhYmZs. Also works as a fragment: #t=90.
    lang / hlfr, enLanguage of the player interface. Defaults to the browser’s (Accept-Language), otherwise English.
    vertical / shorts1, trueForces the vertical layout (blurred background, compact controls). Not needed for vertical videos, which are detected automatically.
    embedded1Reserved for the Bluvy Tube extension: turns autoplay off. Don’t rely on it.
    url
    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.

    html
    <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)

    MessageEffect
    { 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)

    MessageSent 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.
    js
    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.

    http
    GET https://tube.bluvy.app/oembed?url=https%3A%2F%2Ftube.bluvy.app%2Fen%2Fvideo%2F2579051&maxwidth=640
    ParameterEffect
    urlRequired. 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, maxheightOptional. Maximum iframe size, keeping the aspect ratio. Defaults to 560×315, or 315×560 for a vertical video.
    formatOnly json, the default, is available. xml returns a 501.

    Example response:

    json
    {
      "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:

    html
    <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
    <?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.

    http
    GET https://tube.bluvy.app/en/video/2579051/captions/fr.vtt
    GET https://tube.bluvy.app/en/video/2579051/chapters.vtt

    Keyboard shortcuts

    KeyEffectAvailable in
    Space / KPlay / pauseEmbed and site
    MMute / unmuteEmbed and site
    FFullscreenEmbed and site
    IPicture-in-pictureEmbed and site
    ← / →Back / forward 5 sSite video page
    ↑ / ↓VolumeSite video page
    TTheater modeSite video page

    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.

    url
    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
    CollectionRedirects to
    app.bsky.feed.postThe video page, indexed on the fly if needed
    app.bluvy.tube.video.metadataThe video page, indexed on the fly if needed
    app.bluvy.tube.playlistThe playlist page
    app.bsky.actor.profile / DID / handleThe creator’s channel
    place.stream.livestreamThe 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.

    html
    <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 send Content-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:video or a JSON-LD VideoObject yet.
    • 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.

    Link copied to clipboard

    Sign in

    Time to take a break

    You've been watching videos for 30 minutes straight: give yourself 5 minutes away from the screen. Stand up, stretch, grab a glass of water… Your videos will be right here when you're back.