mirror of
https://gitlab.com/vyla-entertainment/documentation.git
synced 2026-11-14 22:47:26 -05:00
huge rewrite
This commit is contained in:
112
guides/error-handling.md
Normal file
112
guides/error-handling.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Error Handling & Resilience Guide
|
||||
|
||||
<p class="page-description">Strategies, error classification, and recovery patterns for handling network stalls, upstream rate limits, and streaming failures.</p>
|
||||
|
||||
## Overview
|
||||
|
||||
Streaming from distributed upstream web providers is inherently dynamic. Upstream CDN nodes rotate domains, regional ISPs implement traffic shaping, and token signatures expire.
|
||||
|
||||
Building a resilient client requires planning for failure at every step. This guide explains how Vyla handles errors internally and how client players should respond.
|
||||
|
||||
---
|
||||
|
||||
## Error Classification Matrix
|
||||
|
||||
| Error Type | Common Cause | Stream API / SDK Behavior | Recommended Player Action |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **HTTP 400 Bad Request** | Missing or malformed TMDB ID, invalid season/episode | Returns immediate JSON error payload without querying providers. | Validate input parameters client-side before sending requests. |
|
||||
| **HTTP 429 Too Many Requests** | Upstream host rate-limiting client IP | SDK provider applies backoff delay and retries up to `retries` limit. | If proxy returns 429, respect the `retry_after` header and back off. |
|
||||
| **HTTP 502 Bad Gateway** | Upstream provider host is unreachable or timed out | Provider is discarded; other scrapers continue executing. | Fall back to the next available stream candidate in your queue. |
|
||||
| **SSE Disconnect** | Network hiccup or client tab backgrounded | Connection drops; `EventSource.onerror` fires. | If video is already playing, do not interrupt. Otherwise, reconnect after 2 seconds. |
|
||||
| **HLS Fatal Media Error** | Segment buffer stall, corrupt chunk, or missing decryption key | Player triggers `Hls.Events.ERROR` with `fatal: true`. | Call `hls.destroy()` and load the next candidate from your fallback queue. |
|
||||
|
||||
---
|
||||
|
||||
## Client Fallback Queue Strategy
|
||||
|
||||
Always treat streaming candidate resolution as a prioritized queue rather than a single static link:
|
||||
|
||||
### Queue Priority Assignment
|
||||
|
||||
| Candidate Stream | Player State | Fallback Precedence |
|
||||
| :--- | :--- | :--- |
|
||||
| **Candidate 1** (`vidlink`) | **Active Playback** | Mounted into HLS.js or ExoPlayer on arrival. |
|
||||
| **Candidate 2** (`vixsrc`) | **Standby Backup** | Enqueued as Priority #1 fallback stream. |
|
||||
| **Candidate 3** (`cinecat`) | **Standby Backup** | Enqueued as Priority #2 fallback stream. |
|
||||
|
||||
### Recovery Sequence on Stream Stall
|
||||
1. **Error Interception**: Player detects a fatal network or decode event (`data.fatal === true`).
|
||||
2. **Timestamp Preservation**: Record current playback offset (e.g. `currentTime = 142.5s`).
|
||||
3. **Buffer Teardown**: Call `hls.destroy()` or `player.release()` to release media pipelines and decoders.
|
||||
4. **Queue Advancement**: Dequeue the next available verified candidate from the standby queue.
|
||||
5. **Seamless Seek**: Mount the fallback source and seek immediately to the preserved timestamp upon `loadedmetadata`.
|
||||
|
||||
### Implementing Fallback Recovery in JavaScript
|
||||
|
||||
```javascript
|
||||
function attachHlsPlayer(videoElement, candidate, fallbackQueue) {
|
||||
const hls = new Hls({
|
||||
maxBufferLength: 30,
|
||||
maxMaxBufferLength: 60,
|
||||
enableWorker: true
|
||||
});
|
||||
|
||||
hls.loadSource(candidate.url);
|
||||
hls.attachMedia(videoElement);
|
||||
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
if (!data.fatal) return;
|
||||
|
||||
console.warn(`Encountered fatal error on ${candidate.source}:`, data.type);
|
||||
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
console.log("Attempting network recovery...");
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log("Attempting media recovery...");
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
hls.destroy();
|
||||
if (fallbackQueue.length > 0) {
|
||||
const nextCandidate = fallbackQueue.shift();
|
||||
const lastTime = videoElement.currentTime;
|
||||
console.warn(`Switching to fallback candidate: ${nextCandidate.label}`);
|
||||
|
||||
attachHlsPlayer(videoElement, nextCandidate, fallbackQueue);
|
||||
videoElement.addEventListener('loadedmetadata', () => {
|
||||
videoElement.currentTime = lastTime;
|
||||
videoElement.play();
|
||||
}, { once: true });
|
||||
} else {
|
||||
showErrorOverlay("All available streaming providers have failed.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return hls;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client-Side Timeout Budgets
|
||||
|
||||
Never leave users hanging on an indefinite spinner. Implement a two-tiered timeout budget:
|
||||
|
||||
1. **Initial Candidate Timeout (8–10 seconds)**: If no `type: "source"` event has arrived within 8 seconds, update the UI to indicate extended searching.
|
||||
2. **Hard Timeout (20 seconds)**: If the stream has not completed or emitted a viable candidate within 20 seconds:
|
||||
- Call `eventSource.close()`.
|
||||
- Render a helpful empty-state UI with a "Retry" button or an option to change the server URL.
|
||||
|
||||
---
|
||||
|
||||
## Decoupled Subtitle Fallback
|
||||
|
||||
Subtitles originate from independent subtitle repositories (e.g. OpenSubtitles, SubDL). If a subtitle track fails to load:
|
||||
- The video playback must continue uninterrupted.
|
||||
- Log a warning in the console.
|
||||
- Offer the viewer an in-player menu to toggle alternative subtitle providers or upload a local `.srt`/`.vtt` file.
|
||||
213
guides/integrations.md
Normal file
213
guides/integrations.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Client Integration Guide
|
||||
|
||||
<p class="page-description">Frontend and native player implementation patterns for integrating Vyla Stream API with HLS.js, React, Vue, Android ExoPlayer, and iOS AVPlayer.</p>
|
||||
|
||||
## Overview
|
||||
|
||||
Stream API exposes standard HTTP and Server-Sent Events (SSE) interfaces, allowing it to integrate with any modern video player library. This guide provides copyable, production-ready integration patterns across multiple platforms and frameworks.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: React Player Hook
|
||||
|
||||
Below is a complete custom React hook (`useVylaStream`) managing SSE event consumption, fallback candidate queuing, and playback state:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface StreamSource {
|
||||
source: string;
|
||||
label: string;
|
||||
url: string;
|
||||
quality: string;
|
||||
type: 'hls' | 'mp4';
|
||||
}
|
||||
|
||||
interface StreamMeta {
|
||||
title: string;
|
||||
year: number;
|
||||
poster: string;
|
||||
}
|
||||
|
||||
export function useVylaStream(apiBase: string, tmdbId: number, season?: number, episode?: number) {
|
||||
const [meta, setMeta] = useState<StreamMeta | null>(null);
|
||||
const [activeSource, setActiveSource] = useState<StreamSource | null>(null);
|
||||
const [subtitles, setSubtitles] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fallbackQueue = useRef<StreamSource[]>([]);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
fallbackQueue.current = [];
|
||||
|
||||
const endpoint = season != null && episode != null
|
||||
? `${apiBase}/tv?id=${tmdbId}&season=${season}&episode=${episode}`
|
||||
: `${apiBase}/movie?id=${tmdbId}`;
|
||||
|
||||
const es = new EventSource(endpoint);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'meta') {
|
||||
setMeta(data.meta);
|
||||
if (data.subtitles) setSubtitles(data.subtitles);
|
||||
}
|
||||
else if (data.type === 'source') {
|
||||
if (!activeSource) {
|
||||
setActiveSource(data);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
fallbackQueue.current.push(data);
|
||||
}
|
||||
}
|
||||
else if (data.type === 'done') {
|
||||
es.close();
|
||||
if (!activeSource && fallbackQueue.current.length === 0) {
|
||||
setError('No available streaming sources found.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
if (!activeSource) {
|
||||
setError('Failed to connect to Stream API.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
es.close();
|
||||
};
|
||||
}, [apiBase, tmdbId, season, episode]);
|
||||
|
||||
const triggerNextFallback = () => {
|
||||
if (fallbackQueue.current.length > 0) {
|
||||
const next = fallbackQueue.current.shift()!;
|
||||
console.warn(`Switching to fallback candidate: ${next.label}`);
|
||||
setActiveSource(next);
|
||||
} else {
|
||||
setError('All provider stream candidates exhausted.');
|
||||
}
|
||||
};
|
||||
|
||||
return { meta, activeSource, subtitles, isLoading, error, triggerNextFallback };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Vue 3 Composable Component
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="player-wrapper">
|
||||
<video ref="videoRef" controls playsinline></video>
|
||||
<div v-if="isLoading" class="loading-overlay">Discovering streams...</div>
|
||||
<div v-if="error" class="error-banner">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import Hls from 'hls.js';
|
||||
|
||||
const props = defineProps<{
|
||||
apiBase: string;
|
||||
tmdbId: number;
|
||||
}>();
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const isLoading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
let hls: Hls | null = null;
|
||||
let es: EventSource | null = null;
|
||||
|
||||
function loadStream(url: string) {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
if (hls) hls.destroy();
|
||||
hls = new Hls({ maxBufferLength: 30 });
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(videoRef.value);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => videoRef.value?.play());
|
||||
} else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
videoRef.value.src = url;
|
||||
videoRef.value.play();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
es = new EventSource(`${props.apiBase}/movie?id=${props.tmdbId}`);
|
||||
let firstSourceReceived = false;
|
||||
|
||||
es.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'source' && !firstSourceReceived) {
|
||||
firstSourceReceived = true;
|
||||
isLoading.value = false;
|
||||
loadStream(data.url);
|
||||
} else if (data.type === 'done') {
|
||||
es?.close();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
es?.close();
|
||||
hls?.destroy();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Android Native (Kotlin & ExoPlayer)
|
||||
|
||||
In Android applications, connect to the Stream API proxy using Google's `MediaItem`:
|
||||
|
||||
```kotlin
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
||||
fun playStream(player: ExoPlayer, proxiedHlsUrl: String) {
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setUri(proxiedHlsUrl)
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()
|
||||
|
||||
player.setMediaItem(mediaItem)
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: iOS / macOS Native (Swift & AVPlayer)
|
||||
|
||||
Because iOS AVPlayer natively handles HLS streams and respects cookies/redirects, assigning the proxied URL is straightforward:
|
||||
|
||||
```swift
|
||||
import AVKit
|
||||
|
||||
func playVylaStream(proxiedUrlString: String) {
|
||||
guard let url = URL(string: proxiedUrlString) else { return }
|
||||
let player = AVPlayer(url: url)
|
||||
let playerViewController = AVPlayerViewController()
|
||||
playerViewController.player = player
|
||||
|
||||
present(playerViewController, animated: true) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user