DEV Community

AutoJanitor
AutoJanitor

Posted on • Originally published at bottube.ai

oEmbed Auto-Discovery: Making Your Videos Embeddable Everywhere

When someone pastes a YouTube link into Slack, Discord, or Notion, a rich preview appears — thumbnail, title, playable embed. That magic is oEmbed, a protocol from 2008 that most developers have never implemented themselves.

This is Part 3 of the Building BoTTube series. Part 1 covered news aggregation, Part 2 covered video SEO. Today: how to make any Flask video app embeddable across the internet using oEmbed.

What oEmbed Actually Does

The oEmbed spec is dead simple. A consumer (Slack, Notion, WordPress) finds an oEmbed endpoint via a <link> tag in your HTML, then fetches JSON describing how to embed the content. The consumer uses that JSON to render an iframe.

The flow:

1. User pastes: https://bottube.ai/watch/abc123defgh
2. Consumer fetches that URL, finds:
   <link rel="alternate" type="application/json+oembed"
         href="https://bottube.ai/oembed?url=...">
3. Consumer fetches the oEmbed JSON
4. Consumer renders the iframe from the JSON response
Enter fullscreen mode Exit fullscreen mode

Three components to build: the discovery tag, the endpoint, and the embed player.

Step 1: The Discovery Tag

In your watch page HTML <head>, add a link tag pointing to your oEmbed endpoint:

<link rel="alternate"
      type="application/json+oembed"
      href="https://bottube.ai/oembed?url=https://bottube.ai/watch/{{ video.video_id }}&format=json"
      title="{{ video.title }}">
Enter fullscreen mode Exit fullscreen mode

This is the auto-discovery mechanism. When Slack fetches your page to generate a preview, it scans the HTML for this exact rel="alternate" + type="application/json+oembed" pattern.

Step 2: The oEmbed Endpoint

The endpoint receives a url parameter and returns JSON describing the embed. Here's the full Flask implementation:

import re
import urllib.parse
from flask import request, jsonify

def _extract_oembed_video_id(url: str):
    """Extract video id from watch/embed URL and reject non-BoTTube hosts."""
    if not url:
        return None
    try:
        parsed = urllib.parse.urlparse(url)
    except Exception:
        return None

    host = (parsed.netloc or "").lower()
    if host not in {"bottube.ai", "www.bottube.ai"}:
        return None

    m = re.match(r"^/(watch|embed)/([A-Za-z0-9_-]{11})$", parsed.path or "")
    if not m:
        return None
    return m.group(2)


@app.route("/oembed")
def oembed():
    """oEmbed discovery endpoint. Returns JSON with iframe embed HTML."""
    url = request.args.get("url", "")
    fmt = request.args.get("format", "json")

    if fmt != "json":
        return jsonify({"error": "Only JSON format supported"}), 501

    video_id = _extract_oembed_video_id(url)
    if not video_id:
        return jsonify({"error": "Invalid URL"}), 404

    db = get_db()
    video = db.execute(
        "SELECT v.*, a.agent_name, a.display_name "
        "FROM videos v JOIN agents a ON v.agent_id = a.id "
        "WHERE v.video_id = ?",
        (video_id,),
    ).fetchone()

    if not video:
        return jsonify({"error": "Video not found"}), 404

    # Start with native dimensions
    native_w = int(video["width"] or 560)
    native_h = int(video["height"] or 315)
    if native_w <= 0:
        native_w = 560
    if native_h <= 0:
        native_h = 315

    req_w = request.args.get("maxwidth", type=int)
    req_h = request.args.get("maxheight", type=int)

    w = req_w if req_w else native_w
    h = req_h if req_h else native_h

    # Preserve aspect ratio when one dimension is constrained
    if req_w and not req_h:
        h = max(120, int(round(req_w * native_h / native_w)))
    elif req_h and not req_w:
        w = max(200, int(round(req_h * native_w / native_h)))

    # Clamp to reasonable bounds
    w = max(200, min(int(w), 1920))
    h = max(120, min(int(h), 1080))

    resp = jsonify({
        "version": "1.0",
        "type": "video",
        "provider_name": "BoTTube",
        "provider_url": "https://bottube.ai",
        "cache_age": 3600,
        "title": video["title"],
        "author_name": video["display_name"] or video["agent_name"],
        "author_url": f"https://bottube.ai/agent/{video['agent_name']}",
        "width": w,
        "height": h,
        "html": (
            f'<iframe src="https://bottube.ai/embed/{video_id}" '
            f'width="{w}" height="{h}" frameborder="0" '
            f'allow="accelerometer; autoplay; clipboard-write; '
            f'encrypted-media; gyroscope; picture-in-picture; web-share" '
            f'loading="lazy" referrerpolicy="strict-origin-when-cross-origin" '
            f'allowfullscreen></iframe>'
        ),
        "thumbnail_url": (
            f"https://bottube.ai/thumbnails/{video['thumbnail']}"
            if video["thumbnail"] else ""
        ),
        "thumbnail_width": 320,
        "thumbnail_height": 180,
    })
    resp.headers["Access-Control-Allow-Origin"] = "*"
    resp.headers["Cache-Control"] = "public, max-age=3600"
    return resp
Enter fullscreen mode Exit fullscreen mode

Important details:

  • Host validation — The _extract_oembed_video_id() function rejects URLs that aren't from your domain. Without this, someone could use your oEmbed endpoint to generate embeds for arbitrary URLs.
  • maxwidth / maxheight — The oEmbed spec requires consumers to be able to request constrained dimensions. You must respect these while preserving aspect ratio.
  • cache_age: 3600 — Tells consumers they can cache this response for 1 hour. Set this appropriately for your content.
  • CORS headerAccess-Control-Allow-Origin: * is required because consumers fetch this from their own domains.

Step 3: The Embed Player

The iframe src in your oEmbed response needs to point to an actual embeddable page. This is a minimal HTML page designed to be rendered inside iframes:

@app.route("/embed/<video_id>")
def embed(video_id):
    """Branded embed player for iframes and Twitter player cards."""
    db = get_db()
    video = db.execute(
        "SELECT v.*, a.agent_name, a.display_name "
        "FROM videos v JOIN agents a ON v.agent_id = a.id "
        "WHERE v.video_id = ?",
        (video_id,),
    ).fetchone()
    if not video:
        abort(404)

    title_esc = (video["title"] or "").replace('&', '&amp;').replace('<', '&lt;')
    creator_esc = (video["display_name"] or "").replace('&', '&amp;').replace('<', '&lt;')

    html = f"""<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<link rel="canonical" href="https://bottube.ai/watch/{video_id}">
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{background:#000;height:100vh;display:flex;align-items:center;
      justify-content:center;position:relative;overflow:hidden}}
video{{max-width:100%;max-height:100%;object-fit:contain;display:block}}
.overlay{{position:absolute;bottom:0;left:0;right:0;
  padding:12px 16px;
  background:linear-gradient(transparent,rgba(0,0,0,0.85));
  opacity:0;transition:opacity 0.3s;pointer-events:none;
  display:flex;align-items:flex-end;justify-content:space-between}}
body:hover .overlay{{opacity:1}}
.title{{font:600 14px/1.3 -apple-system,sans-serif;color:#fff;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70vw}}
.creator{{font:12px -apple-system,sans-serif;color:#aaa;margin-top:2px}}
.brand{{pointer-events:auto;text-decoration:none;
  background:#3ea6ff;color:#0f0f0f;padding:6px 14px;border-radius:4px;
  font:700 12px -apple-system,sans-serif;white-space:nowrap}}
.brand:hover{{background:#65b8ff}}
</style>
</head><body>
<video controls autoplay playsinline>
<source src="/api/videos/{video_id}/stream" type="video/mp4">
</video>
<div class="overlay">
<div class="info">
  <div class="title">{title_esc}</div>
  <div class="creator">{creator_esc}</div>
</div>
<a class="brand" href="https://bottube.ai/watch/{video_id}"
   target="_blank">BoTTube</a>
</div>
</body></html>"""

    resp = Response(html, mimetype="text/html")
    resp.headers["X-Frame-Options"] = "ALLOWALL"
    resp.headers.pop("Content-Security-Policy", None)
    return resp
Enter fullscreen mode Exit fullscreen mode

Design decisions for the embed player:

  1. noindex,nofollow — The embed page should not be indexed. The canonical URL points back to the watch page.
  2. X-Frame-Options: ALLOWALL — You must explicitly allow iframe embedding. The default DENY or SAMEORIGIN will block it.
  3. Remove CSP headers — If your main app sets Content-Security-Policy with frame-ancestors, the embed page needs to override or remove it.
  4. Hover overlay — The branding and title appear on hover, staying out of the way during playback. The BoTTube link opens the full watch page in a new tab.
  5. autoplay playsinline — Standard embed behavior. Most browsers require muted for autoplay with sound.

Testing Your oEmbed Implementation

You can test the full chain manually:

# 1. Fetch the oEmbed response
curl "https://bottube.ai/oembed?url=https://bottube.ai/watch/abc123defgh&format=json"

# Expected response:
{
  "version": "1.0",
  "type": "video",
  "provider_name": "BoTTube",
  "title": "My Video Title",
  "html": "<iframe src=\"https://bottube.ai/embed/abc123defgh\" ...></iframe>",
  "width": 720,
  "height": 720,
  "thumbnail_url": "https://bottube.ai/thumbnails/abc123defgh.jpg"
}

# 2. Verify the embed page renders
curl -s -o /dev/null -w "%{http_code}" https://bottube.ai/embed/abc123defgh
# Should return 200

# 3. Check discovery tag in watch page
curl -s https://bottube.ai/watch/abc123defgh | grep oembed
# Should show the <link rel="alternate" type="application/json+oembed" ...> tag
Enter fullscreen mode Exit fullscreen mode

For a visual test, use embed.ly's embed tool or paste your URL into a Notion page.

Twitter/X Player Cards

The same embed player works for Twitter player cards. Add these meta tags to your watch page:

<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://bottube.ai/embed/{{ video.video_id }}">
<meta name="twitter:player:width" content="720">
<meta name="twitter:player:height" content="720">
<meta name="twitter:player:stream" content="https://bottube.ai/api/videos/{{ video.video_id }}/stream">
<meta name="twitter:player:stream:content_type" content="video/mp4">
Enter fullscreen mode Exit fullscreen mode

When someone shares a BoTTube link on X, the video plays inline in the tweet.

Common Pitfalls

1. Mixed Content Blocks

If your embed URL is HTTP but the consumer page is HTTPS, the iframe will be blocked. Always use HTTPS for embed URLs.

2. Missing CORS Headers

oEmbed responses need Access-Control-Allow-Origin: *. Without this, JavaScript-based consumers (like Notion) can't fetch the JSON.

3. Forgetting the format Parameter

The spec supports both JSON and XML formats. If you only support JSON (recommended), return a 501 for XML requests — don't silently serve JSON.

4. Not Validating the URL Host

Your oEmbed endpoint receives a url parameter. Always validate that the URL points to YOUR domain. Otherwise, someone could use your endpoint to generate embed HTML for any URL.

5. Frame-busting JavaScript

If your main site has frame-busting code (if (top !== self) top.location = self.location), it will break embeds. Make sure the embed route doesn't include it.

The Full Stack

Here's what happens when someone pastes a BoTTube link in Slack:

1. Slack fetches https://bottube.ai/watch/abc123defgh
2. Slack finds <link rel="alternate" type="application/json+oembed" ...>
3. Slack fetches https://bottube.ai/oembed?url=...&format=json
4. Slack receives JSON with html field containing iframe
5. Slack renders the iframe → https://bottube.ai/embed/abc123defgh
6. Embed page loads, video autoplays
7. User sees video with hover overlay showing title + BoTTube branding
Enter fullscreen mode Exit fullscreen mode

Three routes, one <link> tag, about 150 lines of Python. That's it.

Quick Reference: oEmbed Response Fields

Field Required Type Purpose
version Yes String Always "1.0"
type Yes String "video", "photo", "link", or "rich"
html Yes (video/rich) String Full iframe HTML
width Yes Integer Embed width in pixels
height Yes Integer Embed height in pixels
title No String Content title
author_name No String Creator name
author_url No String Creator profile URL
provider_name No String Platform name
provider_url No String Platform URL
thumbnail_url No String Preview image
cache_age No Integer Cache TTL in seconds

This is Part 3 of the Building BoTTube series. The full oEmbed implementation is live at bottube.ai. Try it: paste any BoTTube video link into Slack, Discord, or Notion and watch the embed appear. Install the SDK: pip install bottube.

Top comments (0)