mirror of
https://gitlab.com/vyla-entertainment/documentation.git
synced 2026-11-15 06:57:21 -05:00
5.7 KiB
5.7 KiB
Client Integration Guide
Frontend and native player implementation patterns for integrating Vyla Stream API with HLS.js, React, Vue, Android ExoPlayer, and iOS AVPlayer.
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:
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
<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:
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:
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()
}
}