mirror of
https://gitlab.com/vyla-entertainment/documentation.git
synced 2026-11-15 06:57:21 -05:00
178 lines
7.1 KiB
Markdown
178 lines
7.1 KiB
Markdown
# Building a Custom Player
|
|
|
|
<p class="page-description">Architectural principles, format handling, and engineering guidelines for building resilient video players backed by Vyla infrastructure.</p>
|
|
|
|
## 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](https://github.com/video-dev/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:
|
|
|
|
```html
|
|
<!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
|
|
|
|
- Explore the complete [Client Integration Guide](/guides/integrations) for React, Vue, and ExoPlayer examples.
|
|
- Review error classification in the [Error Handling Guide](/guides/error-handling).
|
|
- Review the SSE event protocol in [Paths & Playback Events](/stream-api/reference/paths).
|