Files
documentation/products/player.md

4.7 KiB
Raw Blame History

Building a Custom Player

Architectural principles, format handling, and engineering guidelines for building video players backed by Vyla infrastructure.

Overview

Vyla handles discovery, scraping, and proxying; your app owns the player. Whether building a web player, desktop shell, or mobile UI, this guide outlines the core playback patterns.


Integration Paths

  • Node.js Backends: Use the Vyla SDK directly in-process via sdk.getStream(). Ideal for custom authentication and server-side tracking.
  • Web / Mobile / TV: Connect to the Stream API gateway via HTTP/SSE (/movie or /tv). Ideal for frontend players requiring a live streaming feed.

Playback Formats

  • HLS Streams (type: "hls"): Adaptive master playlists (.m3u8). Proxied via Stream API (/api?url=...), which rewrites chunk URIs and AES encryption keys with permissive CORS headers. Handled via HLS.js or native Safari.
  • Direct Video (type: "mp4"): Direct MP4 or MKV files. Proxied with HTTP Range forwarding (206 Partial Content) for scrubbing.

Player Guidelines

  1. Start on First Source: Begin playback as soon as the first source event arrives (3–8s). Queue later events as fallbacks.
  2. Auto-Failover: If the current stream fails, catch fatal playback errors and immediately switch to the next queued source.
  3. Async Subtitles: Fetch subtitles in the background without blocking video buffering.
  4. Watchdog Timer: If no working source resolves within 15–20s, show an empty state with a retry option.
  5. Clean Teardown: When switching media, close the SSE connection and destroy the HLS instance before opening a new stream.

Minimal Web Player Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Vyla Custom Player</title>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
  <style>
    video { width: 100%; max-width: 960px; height: auto; background: #000; border-radius: 8px; }
    .status { font-family: sans-serif; margin-top: 8px; color: #666; }
  </style>
</head>
<body>
  <video id="vyla-video" controls playsinline></video>
  <div id="status" class="status">Connecting to Stream API...</div>

  <script>
    const API_BASE = "https://api.vyla.cc";
    const TMDB_ID = 155;

    const video = document.getElementById("vyla-video");
    const statusEl = document.getElementById("status");

    const fallbackSources = [];
    let isPlaying = false;
    let hlsInstance = null;

    const eventSource = new EventSource(`${API_BASE}/movie?id=${TMDB_ID}`);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);

      if (data.type === "meta") {
        statusEl.textContent = `Found: ${data.meta.title} (${data.meta.year})`;
        if (data.subtitles) loadSubtitles(data.subtitles);
      } 
      else if (data.type === "source") {
        if (!isPlaying) {
          playCandidate(data);
        } else {
          fallbackSources.push(data);
        }
      } 
      else if (data.type === "done") {
        eventSource.close();
        if (!isPlaying && fallbackSources.length === 0) {
          statusEl.textContent = "No compatible streaming sources found.";
        }
      }
    };

    eventSource.onerror = () => {
      eventSource.close();
      if (!isPlaying) statusEl.textContent = "Error connecting to Stream API.";
    };

    function playCandidate(candidate) {
      isPlaying = true;
      statusEl.textContent = `Streaming from provider: ${candidate.source} (${candidate.quality})`;

      if (candidate.type === "hls") {
        if (Hls.isSupported()) {
          if (hlsInstance) hlsInstance.destroy();
          hlsInstance = new Hls({ maxBufferLength: 30 });
          hlsInstance.loadSource(candidate.url);
          hlsInstance.attachMedia(video);
          hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => video.play());

          hlsInstance.on(Hls.Events.ERROR, (event, errorData) => {
            if (errorData.fatal) {
              hlsInstance.destroy();
              isPlaying = false;
              if (fallbackSources.length > 0) playCandidate(fallbackSources.shift());
            }
          });
        } else if (video.canPlayType("application/vnd.apple.mpegurl")) {
          video.src = candidate.url;
          video.play();
        }
      } else {
        video.src = candidate.url;
        video.play();
      }
    }

    function loadSubtitles(subtitles) {
      subtitles.forEach((sub) => {
        const track = document.createElement("track");
        track.kind = "subtitles";
        track.label = sub.label;
        track.srclang = sub.label.slice(0, 2).toLowerCase();
        track.src = sub.file;
        video.appendChild(track);
      });
    }
  </script>
</body>
</html>