mirror of
https://gitlab.com/vyla-entertainment/documentation.git
synced 2026-11-15 06:57:21 -05:00
214 lines
5.7 KiB
Markdown
214 lines
5.7 KiB
Markdown
# 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()
|
|
}
|
|
}
|
|
```
|