Files
documentation/products/player.md
2026-11-14 18:21:12 -08:00

7.1 KiB

Building a Custom Player

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

Overview

In the Vyla ecosystem, you are never locked into an official frontend. Vyla provides the discovery, resolution, and proxy layers; your application owns the player experience. Whether you are building a custom web portal, desktop client, or mobile app, this guide covers the core engineering patterns required for a robust player.


Integration Boundaries

Choose your integration point based on your architecture:

Architecture Integration Point Data Flow Best For
Node.js Backend Vyla SDK Directly sdk.getStream() in-process Applications with full backend control, custom authentication, and server-side session tracking.
Browser / Mobile Stream API Gateway SSE /movie or /tv over HTTP Web applications, smart TV frontends, or lightweight mobile clients requiring an HTTP/SSE source feed.

Playback Formats: HLS vs Direct Video

Resolved candidate streams come in two primary container types:

  1. HLS Streams (type: "hls"):
    • Master M3U8 playlists containing multiple bitrate/resolution variants.
    • Proxied through Stream API (/api?url=...), which automatically rewrites chunk URLs and AES-128 encryption key URIs.
    • Requires native HLS support (Safari on macOS/iOS) or a JavaScript HLS MSE library such as HLS.js in modern browsers.
  2. Direct Media Files (type: "mp4"):
    • Progressive MP4 or MKV streams.
    • Proxied with HTTP Range header forwarding (206 Partial Content), allowing smooth scrubbing and seeking.
    • Directly assignable to the HTML5 <video> element src attribute.

Resilient Player Architecture

Upstream streaming hosts are dynamic: network latencies fluctuate, upstream proxies fail, and geo-restrictions vary. A production-ready Vyla player must adhere to five design rules:

1. Start on the First Candidate

Never wait for all providers to finish. Stream API emits verified sources over Server-Sent Events (SSE). The moment the first source event arrives (typically within 3 to 8 seconds):

  • Initialize your player immediately.
  • Store any subsequent incoming source events in a priority fallback array.

2. Maintain a Fallback Priority Array

If the active stream stalls, encounters an unrecoverable network error, or buffer-underruns:

  • Catch the error event (e.g. Hls.Events.ERROR with fatal flag).
  • Pop the next candidate from your fallback queue.
  • Re-attach the media source without disrupting the user's viewing session.

3. Decouple Subtitles from Video Playback

Subtitles are retrieved independently from the video stream. Never block initial video buffering waiting for subtitle tracks to download. Load the video stream immediately and inject subtitle tracks (<track kind="subtitles">) asynchronously once resolved.

4. Implement a Client-Side Timeout

Always set a client-side watchdog timer (typically 15 to 20 seconds). If no viable candidate emits a valid video stream before the timer expires, render an informative empty state allowing the user to retry or switch servers.

5. Clean Up on Title Switch

When a viewer selects a new movie or episode:

  • Call eventSource.close() on the active SSE connection.
  • Invoke hls.destroy() or video.removeAttribute('src').
  • Clear pending retry timers and abort in-flight fetch requests.

Reference Implementation: Minimal Web Player

Below is a complete, copyable implementation using native HTML5 and HLS.js:

<!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://your-stream-api.example.com";
    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) {
              console.warn("HLS fatal error encountered. Switching to fallback candidate...");
              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>

Next Steps