Files
documentation/guides/integrations/index.html

1253 lines
105 KiB
HTML

<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stream API client integration — Vyla</title>
<meta name="description"
content="Build an optional Stream API player client with HLS/MP4 handling, SSE sources, subtitles, and fallbacks." />
<meta name="keywords"
content="client integration, player integration, HLS.js, React, Vue, iOS, Android, SSE client, streaming client" />
<link rel="canonical" href="https://vyla.cc/guides/integrations.html" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://vyla.cc/guides/integrations.html" />
<meta property="og:title" content="Stream API client integration — Vyla" />
<meta property="og:description"
content="Build an optional Stream API player client with HLS/MP4 handling, SSE sources, subtitles, and fallbacks." />
<meta property="og:image" content="https://vyla.cc/assets/images/thumbnail.png" />
<meta property="og:site_name" content="Vyla" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://vyla.cc/guides/integrations.html" />
<meta property="twitter:title" content="Stream API client integration — Vyla" />
<meta property="twitter:description"
content="Build an optional Stream API player client with HLS/MP4 handling, SSE sources, subtitles, and fallbacks." />
<meta property="twitter:image" content="https://vyla.cc/assets/images/thumbnail.png" />
<meta property="twitter:site" content="@vylaentertainment" />
<!-- Additional SEO -->
<meta name="robots" content="index, follow" />
<meta name="author" content="Vyla" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/assets/base.css" />
<link rel="stylesheet" href="/assets/highlight.css" />
<link rel="stylesheet" href="/assets/mintlify-overrides.css" />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Stream API client integration",
"description": "Build an optional Stream API player client with HLS/MP4 handling, SSE sources, subtitles, and fallbacks.",
"url": "https://vyla.cc/guides/integrations.html",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Windows, macOS, Linux, Android, Android TV",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
<div id="banner">
<span>Vyla is self-hosted. Run Vyla Home, embed the SDK, or deploy Stream API on infrastructure you control. Rights
holders can request source removal — <a href="/misc/dmca">DMCA policy</a>.</span>
<button id="banner-close" onclick="dismissBanner()" aria-label="Dismiss">✕</button>
</div>
<header id="navbar">
<div class="nav-top">
<a class="brand" href="/"><img src="/assets/images/logo.png" alt="Vyla" id="brand-logo" loading="lazy"
fetchpriority="low"></a>
<button id="search-trigger" type="button" onclick="openSearch()" aria-label="Search"></button>
<div class="links">
<button id="theme-toggle" type="button" onclick="toggleThemeMenu(event)" aria-label="Theme"></button>
</div>
</div>
<nav class="tabs">
<a class="tab-link" href="/">Vyla</a>
<a class="tab-link" href="/sdk">SDK</a>
<a class="tab-link active" href="/stream-api/overview">Stream API</a>
<a class="tab-link" href="/guides/integrations">Resources</a>
<a class="tab-link" href="/misc/credit">Misc</a>
</nav>
</header>
<div id="layout">
<nav id="sidebar">
<div class="group">
<p class="group-title">Guides</p>
<ul>
<li><a class="active" href="/guides/integrations">Integrations</a></li>
<li><a class="" href="/guides/error-handling">Error Handling</a></li>
</ul>
</div>
</nav>
<div id="content-col">
<main class="prose">
<h1>Stream API client integration</h1>
<p class="page-description">Build an optional Stream API player client with HLS/MP4 handling, SSE sources,
subtitles, and fallbacks.</p>
<p>Every source URL from <code>/movie</code> and <code>/tv</code> is either an <strong>HLS stream</strong>
(<code>.m3u8</code>) or an <strong>MP4</strong> file — your player code must handle both. HLS goes through
HLS.js; MP4 sets <code>video.src</code> directly. The detection logic is the same everywhere.</p>
<div class="callout callout-note">
<div class="callout-content">
<p>These examples are for clients that use the optional self-hosted Stream API. If your product already has
a Node.js backend, prefer <a href="/sdk">Vyla SDK</a> directly and expose only the API your product
needs.</p>
</div>
</div>
<hr>
<h2 id="source-type-detection">Source Type Detection</h2>
<p>This is the single utility everything below depends on. A URL is MP4 if the proxy path ends in
<code>.mp4</code> or the upstream served a video content-type that isn't HLS.
</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">isMp4</span>(<span class="hljs-params"><span class="hljs-attr">url</span>: <span class="hljs-built_in">string</span></span>): <span class="hljs-built_in">boolean</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> inner = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(url).<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">'url'</span>) ?? url;
<span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.mp4(\?|$)/i</span>.<span class="hljs-title function_">test</span>(inner) || <span class="hljs-regexp">/\.mkv(\?|$)/i</span>.<span class="hljs-title function_">test</span>(inner);
} <span class="hljs-keyword">catch</span> {
<span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.mp4(\?|$)/i</span>.<span class="hljs-title function_">test</span>(url);
}
}
</code></pre>
<hr>
<h2 id="installation">Installation</h2>
<div class="code-group">
<div class="code-group-tabs"><button class="code-group-tab active" data-tab-index="0" type="button"
onclick="cgSwitch(this)">npm</button><button class="code-group-tab" data-tab-index="1" type="button"
onclick="cgSwitch(this)">yarn</button><button class="code-group-tab" data-tab-index="2" type="button"
onclick="cgSwitch(this)">CDN</button></div>
<div class="code-group-panels">
<div class="code-group-panel active">
<pre><code class="hljs language-bash">npm install hls.js</code></pre>
</div>
<div class="code-group-panel">
<pre><code class="hljs language-bash">yarn add hls.js</code></pre>
</div>
<div class="code-group-panel">
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span></code></pre>
</div>
</div>
</div>
<hr>
<h2 id="how-the-stream-works">How the Stream Works</h2>
<p><code>/movie</code> and <code>/tv</code> are <strong>Server-Sent Events</strong> — not JSON. Each response is
a live stream of <code>data:</code> lines. Three event types arrive in sequence:</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>When</th>
<th>Contains</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>meta</code></td>
<td>Immediately, before any provider resolves</td>
<td>TMDB metadata + all subtitle tracks</td>
</tr>
<tr>
<td><code>source</code></td>
<td>Once per working provider, as each resolves</td>
<td>Proxied URL (HLS <strong>or</strong> MP4) + provider label</td>
</tr>
<tr>
<td><code>done</code></td>
<td>After all providers finish or time out</td>
<td>Total working source count</td>
</tr>
</tbody>
</table>
<p>Start playback on the <strong>first <code>source</code> event</strong> — don't wait for <code>done</code>.
Queue every subsequent <code>source</code> as a fallback.</p>
<hr>
<h2 id="minimal-html-player">Minimal HTML Player</h2>
<p>Zero dependencies beyond HLS.js. Handles both HLS and MP4 correctly.</p>
<pre><code class="hljs language-html"><span class="hljs-meta">&#x3C;!DOCTYPE <span class="hljs-keyword">html</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">html</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">head</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">head</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">body</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">video</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"player"</span> <span class="hljs-attr">controls</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"width: 100%; max-width: 900px;"</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">video</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"status"</span>></span>Loading…<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">script</span>></span><span class="javascript">
<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">BASE</span> = <span class="hljs-string">'http://localhost:7860'</span>;
<span class="hljs-keyword">const</span> video = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'player'</span>);
<span class="hljs-keyword">const</span> status = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'status'</span>);
<span class="hljs-keyword">let</span> hlsInstance = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">function</span> <span class="hljs-title function_">isMp4</span>(<span class="hljs-params">url</span>) {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> inner = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(url).<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">'url'</span>) ?? url;
<span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(inner);
} <span class="hljs-keyword">catch</span> { <span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(url); }
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">attachSource</span>(<span class="hljs-params">url, label</span>) {
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Loading <span class="hljs-subst">${label}</span>…`</span>;
hlsInstance?.<span class="hljs-title function_">destroy</span>();
hlsInstance = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">if</span> (<span class="hljs-title function_">isMp4</span>(url)) {
video.<span class="hljs-property">src</span> = url;
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span> (MP4)`</span>;
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">if</span> (<span class="hljs-title class_">Hls</span>.<span class="hljs-title function_">isSupported</span>()) {
hlsInstance = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Hls</span>();
hlsInstance.<span class="hljs-title function_">loadSource</span>(url);
hlsInstance.<span class="hljs-title function_">attachMedia</span>(video);
hlsInstance.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">MANIFEST_PARSED</span>, <span class="hljs-function">() =></span> {
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span>`</span>;
});
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">if</span> (video.<span class="hljs-title function_">canPlayType</span>(<span class="hljs-string">'application/vnd.apple.mpegurl'</span>)) {
video.<span class="hljs-property">src</span> = url;
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span>`</span>;
<span class="hljs-keyword">return</span>;
}
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">'HLS not supported in this browser.'</span>;
}
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">play</span>(<span class="hljs-params">tmdbId</span>) {
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">`<span class="hljs-subst">${BASE}</span>/movie?id=<span class="hljs-subst">${tmdbId}</span>`</span>);
<span class="hljs-keyword">const</span> reader = res.<span class="hljs-property">body</span>.<span class="hljs-title function_">getReader</span>();
<span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TextDecoder</span>();
<span class="hljs-keyword">let</span> buffer = <span class="hljs-string">''</span>;
<span class="hljs-keyword">let</span> started = <span class="hljs-literal">false</span>;
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
<span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.<span class="hljs-title function_">read</span>();
<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;
buffer += decoder.<span class="hljs-title function_">decode</span>(value, { <span class="hljs-attr">stream</span>: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">const</span> lines = buffer.<span class="hljs-title function_">split</span>(<span class="hljs-string">'\n'</span>);
buffer = lines.<span class="hljs-title function_">pop</span>();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
<span class="hljs-keyword">if</span> (!line.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">'data: '</span>)) <span class="hljs-keyword">continue</span>;
<span class="hljs-keyword">const</span> event = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(line.<span class="hljs-title function_">slice</span>(<span class="hljs-number">6</span>));
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'source'</span> &#x26;&#x26; !started) {
started = <span class="hljs-literal">true</span>;
<span class="hljs-title function_">attachSource</span>(event.<span class="hljs-property">source</span>.<span class="hljs-property">url</span>, event.<span class="hljs-property">source</span>.<span class="hljs-property">label</span>);
}
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'done'</span> &#x26;&#x26; !started) {
status.<span class="hljs-property">textContent</span> = <span class="hljs-string">'No sources available.'</span>;
}
}
}
}
<span class="hljs-title function_">play</span>(<span class="hljs-number">550</span>);
</span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">body</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">html</span>></span>
</code></pre>
<hr>
<h2 id="html-player--full-featured">HTML Player — Full Featured</h2>
<p>Source switching, fallback queue, subtitle tracks, quality selector. All source types handled.</p>
<pre><code class="hljs language-html"><span class="hljs-meta">&#x3C;!DOCTYPE <span class="hljs-keyword">html</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">html</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">head</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">style</span>></span><span class="css">
* { <span class="hljs-attribute">box-sizing</span>: border-box; }
<span class="hljs-selector-tag">body</span> { <span class="hljs-attribute">font-family</span>: sans-serif; <span class="hljs-attribute">background</span>: <span class="hljs-number">#0f0f0f</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#fff</span>; <span class="hljs-attribute">padding</span>: <span class="hljs-number">1rem</span>; <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>; }
<span class="hljs-selector-tag">video</span> { <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>; <span class="hljs-attribute">max-width</span>: <span class="hljs-number">960px</span>; <span class="hljs-attribute">display</span>: block; <span class="hljs-attribute">background</span>: <span class="hljs-number">#000</span>; <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">6px</span>; }
<span class="hljs-selector-id">#controls</span> { <span class="hljs-attribute">max-width</span>: <span class="hljs-number">960px</span>; <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">0.5rem</span>; <span class="hljs-attribute">display</span>: flex; <span class="hljs-attribute">gap</span>: <span class="hljs-number">0.5rem</span>; <span class="hljs-attribute">flex-wrap</span>: wrap; <span class="hljs-attribute">align-items</span>: center; }
<span class="hljs-selector-id">#sources</span> { <span class="hljs-attribute">display</span>: flex; <span class="hljs-attribute">gap</span>: <span class="hljs-number">0.4rem</span>; <span class="hljs-attribute">flex-wrap</span>: wrap; <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">0.4rem</span>; <span class="hljs-attribute">max-width</span>: <span class="hljs-number">960px</span>; }
<span class="hljs-selector-tag">button</span> { <span class="hljs-attribute">padding</span>: <span class="hljs-number">0.35rem</span> <span class="hljs-number">0.75rem</span>; <span class="hljs-attribute">background</span>: <span class="hljs-number">#1e1e1e</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#ccc</span>; <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#333</span>;
<span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>; <span class="hljs-attribute">cursor</span>: pointer; <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.8rem</span>; }
<span class="hljs-selector-tag">button</span><span class="hljs-selector-pseudo">:hover</span> { <span class="hljs-attribute">background</span>: <span class="hljs-number">#2a2a2a</span>; }
<span class="hljs-selector-tag">button</span><span class="hljs-selector-class">.active</span> { <span class="hljs-attribute">background</span>: <span class="hljs-number">#fff</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#111</span>; <span class="hljs-attribute">border-color</span>: <span class="hljs-number">#fff</span>; }
<span class="hljs-selector-tag">select</span> { <span class="hljs-attribute">background</span>: <span class="hljs-number">#1e1e1e</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#ccc</span>; <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#333</span>; <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
<span class="hljs-attribute">padding</span>: <span class="hljs-number">0.35rem</span> <span class="hljs-number">0.5rem</span>; <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.8rem</span>; }
<span class="hljs-selector-id">#status</span> { <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.8rem</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#666</span>; <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">0.4rem</span>; <span class="hljs-attribute">max-width</span>: <span class="hljs-number">960px</span>; }
<span class="hljs-selector-class">.badge</span> { <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.65rem</span>; <span class="hljs-attribute">background</span>: <span class="hljs-number">#333</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#aaa</span>; <span class="hljs-attribute">padding</span>: <span class="hljs-number">1px</span> <span class="hljs-number">5px</span>;
<span class="hljs-attribute">border-radius</span>: <span class="hljs-number">3px</span>; <span class="hljs-attribute">margin-left</span>: <span class="hljs-number">4px</span>; <span class="hljs-attribute">vertical-align</span>: middle; }
</span><span class="hljs-tag">&#x3C;/<span class="hljs-name">style</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">head</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">body</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">video</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"player"</span> <span class="hljs-attr">controls</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">video</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"controls"</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">select</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"quality"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"display:none"</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"-1"</span>></span>Quality: Auto<span class="hljs-tag">&#x3C;/<span class="hljs-name">option</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">select</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"sources"</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"status"</span>></span>Connecting…<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">script</span>></span><span class="javascript">
<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">BASE</span> = <span class="hljs-string">'http://localhost:7860'</span>;
<span class="hljs-keyword">const</span> video = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'player'</span>);
<span class="hljs-keyword">const</span> statusEl = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'status'</span>);
<span class="hljs-keyword">const</span> sourcesEl = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'sources'</span>);
<span class="hljs-keyword">const</span> qualitySel = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'quality'</span>);
<span class="hljs-keyword">let</span> hlsInstance = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">let</span> allSources = [];
<span class="hljs-keyword">let</span> activeUrl = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">function</span> <span class="hljs-title function_">isMp4</span>(<span class="hljs-params">url</span>) {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> inner = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(url).<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">'url'</span>) ?? url;
<span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(inner);
} <span class="hljs-keyword">catch</span> { <span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(url); }
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">setActive</span>(<span class="hljs-params">url</span>) {
activeUrl = url;
sourcesEl.<span class="hljs-title function_">querySelectorAll</span>(<span class="hljs-string">'button'</span>).<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">b</span> =></span> {
b.<span class="hljs-property">classList</span>.<span class="hljs-title function_">toggle</span>(<span class="hljs-string">'active'</span>, b.<span class="hljs-property">dataset</span>.<span class="hljs-property">url</span> === url);
});
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">switchSource</span>(<span class="hljs-params">url, label</span>) {
<span class="hljs-title function_">setActive</span>(url);
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Loading <span class="hljs-subst">${label}</span>…`</span>;
qualitySel.<span class="hljs-property">style</span>.<span class="hljs-property">display</span> = <span class="hljs-string">'none'</span>;
qualitySel.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">'&#x3C;option value="-1">Quality: Auto&#x3C;/option>'</span>;
hlsInstance?.<span class="hljs-title function_">destroy</span>();
hlsInstance = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">if</span> (<span class="hljs-title function_">isMp4</span>(url)) {
video.<span class="hljs-property">src</span> = url;
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span> · MP4`</span>;
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">if</span> (<span class="hljs-title class_">Hls</span>.<span class="hljs-title function_">isSupported</span>()) {
hlsInstance = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Hls</span>({ <span class="hljs-attr">startLevel</span>: -<span class="hljs-number">1</span> });
hlsInstance.<span class="hljs-title function_">loadSource</span>(url);
hlsInstance.<span class="hljs-title function_">attachMedia</span>(video);
hlsInstance.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">MANIFEST_PARSED</span>, <span class="hljs-function">(<span class="hljs-params">_, data</span>) =></span> {
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span> · HLS`</span>;
<span class="hljs-keyword">if</span> (data.<span class="hljs-property">levels</span>.<span class="hljs-property">length</span> > <span class="hljs-number">1</span>) {
data.<span class="hljs-property">levels</span>.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">level, i</span>) =></span> {
<span class="hljs-keyword">const</span> opt = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'option'</span>);
opt.<span class="hljs-property">value</span> = i;
opt.<span class="hljs-property">textContent</span> = level.<span class="hljs-property">height</span> ? <span class="hljs-string">`<span class="hljs-subst">${level.height}</span>p`</span> : <span class="hljs-string">`Level <span class="hljs-subst">${i}</span>`</span>;
qualitySel.<span class="hljs-title function_">appendChild</span>(opt);
});
qualitySel.<span class="hljs-property">style</span>.<span class="hljs-property">display</span> = <span class="hljs-string">''</span>;
}
});
hlsInstance.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">LEVEL_SWITCHED</span>, <span class="hljs-function">(<span class="hljs-params">_, data</span>) =></span> {
qualitySel.<span class="hljs-property">value</span> = hlsInstance.<span class="hljs-property">autoLevelEnabled</span> ? <span class="hljs-string">'-1'</span> : data.<span class="hljs-property">level</span>;
});
hlsInstance.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">ERROR</span>, <span class="hljs-function">(<span class="hljs-params">_, err</span>) =></span> {
<span class="hljs-keyword">if</span> (err.<span class="hljs-property">fatal</span>) {
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`<span class="hljs-subst">${label}</span> failed — trying next…`</span>;
<span class="hljs-title function_">tryFallback</span>(url);
}
});
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">if</span> (video.<span class="hljs-title function_">canPlayType</span>(<span class="hljs-string">'application/vnd.apple.mpegurl'</span>)) {
video.<span class="hljs-property">src</span> = url;
video.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Playing <span class="hljs-subst">${label}</span> · HLS`</span>;
<span class="hljs-keyword">return</span>;
}
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">'HLS not supported — try a different browser.'</span>;
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">tryFallback</span>(<span class="hljs-params">failedUrl</span>) {
<span class="hljs-keyword">const</span> next = allSources.<span class="hljs-title function_">find</span>(<span class="hljs-function"><span class="hljs-params">s</span> =></span> s.<span class="hljs-property">url</span> !== failedUrl &#x26;&#x26; s.<span class="hljs-property">url</span> !== activeUrl);
<span class="hljs-keyword">if</span> (next) <span class="hljs-title function_">switchSource</span>(next.<span class="hljs-property">url</span>, next.<span class="hljs-property">label</span>);
<span class="hljs-keyword">else</span> statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">'All sources failed.'</span>;
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">addSourceButton</span>(<span class="hljs-params">source</span>) {
allSources.<span class="hljs-title function_">push</span>(source);
<span class="hljs-keyword">const</span> { url, label } = source;
<span class="hljs-keyword">const</span> btn = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'button'</span>);
btn.<span class="hljs-property">dataset</span>.<span class="hljs-property">url</span> = url;
btn.<span class="hljs-property">innerHTML</span> = label + (<span class="hljs-title function_">isMp4</span>(url) ? <span class="hljs-string">'&#x3C;span class="badge">MP4&#x3C;/span>'</span> : <span class="hljs-string">'&#x3C;span class="badge">HLS&#x3C;/span>'</span>);
btn.<span class="hljs-property">onclick</span> = <span class="hljs-function">() =></span> <span class="hljs-title function_">switchSource</span>(url, label);
sourcesEl.<span class="hljs-title function_">appendChild</span>(btn);
}
qualitySel.<span class="hljs-property">onchange</span> = <span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (hlsInstance) hlsInstance.<span class="hljs-property">currentLevel</span> = <span class="hljs-built_in">parseInt</span>(qualitySel.<span class="hljs-property">value</span>);
};
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">loadContent</span>(<span class="hljs-params">tmdbId, season, episode</span>) {
<span class="hljs-keyword">const</span> url = (season &#x26;&#x26; episode)
? <span class="hljs-string">`<span class="hljs-subst">${BASE}</span>/tv?id=<span class="hljs-subst">${tmdbId}</span>&#x26;season=<span class="hljs-subst">${season}</span>&#x26;episode=<span class="hljs-subst">${episode}</span>`</span>
: <span class="hljs-string">`<span class="hljs-subst">${BASE}</span>/movie?id=<span class="hljs-subst">${tmdbId}</span>`</span>;
allSources = [];
sourcesEl.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">''</span>;
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">'Connecting…'</span>;
<span class="hljs-keyword">let</span> started = <span class="hljs-literal">false</span>;
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url);
<span class="hljs-keyword">const</span> reader = res.<span class="hljs-property">body</span>.<span class="hljs-title function_">getReader</span>();
<span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TextDecoder</span>();
<span class="hljs-keyword">let</span> buffer = <span class="hljs-string">''</span>;
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
<span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.<span class="hljs-title function_">read</span>();
<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;
buffer += decoder.<span class="hljs-title function_">decode</span>(value, { <span class="hljs-attr">stream</span>: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">const</span> lines = buffer.<span class="hljs-title function_">split</span>(<span class="hljs-string">'\n'</span>);
buffer = lines.<span class="hljs-title function_">pop</span>();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
<span class="hljs-keyword">if</span> (!line.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">'data: '</span>)) <span class="hljs-keyword">continue</span>;
<span class="hljs-keyword">const</span> event = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(line.<span class="hljs-title function_">slice</span>(<span class="hljs-number">6</span>));
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'meta'</span>) {
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">`Loading <span class="hljs-subst">${event.meta?.title ?? event.meta?.name ?? <span class="hljs-string">''</span>}</span>…`</span>;
<span class="hljs-title class_">Array</span>.<span class="hljs-title function_">from</span>(video.<span class="hljs-title function_">querySelectorAll</span>(<span class="hljs-string">'track'</span>)).<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">t</span> =></span> t.<span class="hljs-title function_">remove</span>());
(event.<span class="hljs-property">subtitles</span> ?? []).<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">sub, i</span>) =></span> {
<span class="hljs-keyword">const</span> track = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">assign</span>(<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'track'</span>), {
<span class="hljs-attr">kind</span>: <span class="hljs-string">'subtitles'</span>, <span class="hljs-attr">label</span>: sub.<span class="hljs-property">label</span>, <span class="hljs-attr">src</span>: sub.<span class="hljs-property">file</span>,
});
<span class="hljs-keyword">if</span> (i === <span class="hljs-number">0</span>) track.<span class="hljs-property">default</span> = <span class="hljs-literal">true</span>;
video.<span class="hljs-title function_">appendChild</span>(track);
});
}
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'source'</span>) {
<span class="hljs-title function_">addSourceButton</span>(event.<span class="hljs-property">source</span>);
<span class="hljs-keyword">if</span> (!started) {
started = <span class="hljs-literal">true</span>;
<span class="hljs-title function_">switchSource</span>(event.<span class="hljs-property">source</span>.<span class="hljs-property">url</span>, event.<span class="hljs-property">source</span>.<span class="hljs-property">label</span>);
}
}
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'done'</span> &#x26;&#x26; !started) {
statusEl.<span class="hljs-property">textContent</span> = <span class="hljs-string">'No sources available.'</span>;
}
}
}
}
<span class="hljs-title function_">loadContent</span>(<span class="hljs-number">550</span>);
</span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">body</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">html</span>></span>
</code></pre>
<hr>
<h2 id="react">React</h2>
<h3 id="usestream-hook"><code>useStream</code> Hook</h3>
<p>Handles SSE parsing, abort on unmount, and re-fetch on param change. Drop-in for any framework.</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">import</span> { useState, useEffect, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">BASE</span> = <span class="hljs-string">'http://localhost:7860'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">StreamSource</span> {
<span class="hljs-attr">source</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">label</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">url</span>: <span class="hljs-built_in">string</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">StreamSubtitle</span> {
<span class="hljs-attr">label</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">file</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">type</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">source</span>: <span class="hljs-built_in">string</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">StreamState</span> {
<span class="hljs-attr">sources</span>: <span class="hljs-title class_">StreamSource</span>[];
<span class="hljs-attr">subtitles</span>: <span class="hljs-title class_">StreamSubtitle</span>[];
<span class="hljs-attr">meta</span>: <span class="hljs-title class_">Record</span>&#x3C;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">unknown</span>> | <span class="hljs-literal">null</span>;
<span class="hljs-attr">loading</span>: <span class="hljs-built_in">boolean</span>;
<span class="hljs-attr">done</span>: <span class="hljs-built_in">boolean</span>;
<span class="hljs-attr">error</span>: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">useStream</span>(<span class="hljs-params">
<span class="hljs-attr">tmdbId</span>: <span class="hljs-built_in">number</span> | <span class="hljs-literal">null</span>,
<span class="hljs-attr">season</span>?: <span class="hljs-built_in">number</span> | <span class="hljs-literal">null</span>,
<span class="hljs-attr">episode</span>?: <span class="hljs-built_in">number</span> | <span class="hljs-literal">null</span>,
</span>): <span class="hljs-title class_">StreamState</span> {
<span class="hljs-keyword">const</span> [state, setState] = useState&#x3C;<span class="hljs-title class_">StreamState</span>>({
<span class="hljs-attr">sources</span>: [], <span class="hljs-attr">subtitles</span>: [], <span class="hljs-attr">meta</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">loading</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">done</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-literal">null</span>,
});
<span class="hljs-keyword">const</span> abortRef = useRef&#x3C;<span class="hljs-title class_">AbortController</span> | <span class="hljs-literal">null</span>>(<span class="hljs-literal">null</span>);
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (!tmdbId) <span class="hljs-keyword">return</span>;
abortRef.<span class="hljs-property">current</span>?.<span class="hljs-title function_">abort</span>();
<span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
abortRef.<span class="hljs-property">current</span> = controller;
<span class="hljs-keyword">const</span> url = season &#x26;&#x26; episode
? <span class="hljs-string">`<span class="hljs-subst">${BASE}</span>/tv?id=<span class="hljs-subst">${tmdbId}</span>&#x26;season=<span class="hljs-subst">${season}</span>&#x26;episode=<span class="hljs-subst">${episode}</span>`</span>
: <span class="hljs-string">`<span class="hljs-subst">${BASE}</span>/movie?id=<span class="hljs-subst">${tmdbId}</span>`</span>;
<span class="hljs-title function_">setState</span>({ <span class="hljs-attr">sources</span>: [], <span class="hljs-attr">subtitles</span>: [], <span class="hljs-attr">meta</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">loading</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">done</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-literal">null</span> });
(<span class="hljs-title function_">async</span> () => {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url, {
<span class="hljs-attr">signal</span>: controller.<span class="hljs-property">signal</span>,
});
<span class="hljs-keyword">const</span> reader = res.<span class="hljs-property">body</span>!.<span class="hljs-title function_">getReader</span>();
<span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TextDecoder</span>();
<span class="hljs-keyword">let</span> buffer = <span class="hljs-string">''</span>;
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
<span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.<span class="hljs-title function_">read</span>();
<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;
buffer += decoder.<span class="hljs-title function_">decode</span>(value, { <span class="hljs-attr">stream</span>: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">const</span> lines = buffer.<span class="hljs-title function_">split</span>(<span class="hljs-string">'\n'</span>);
buffer = lines.<span class="hljs-title function_">pop</span>()!;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
<span class="hljs-keyword">if</span> (!line.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">'data: '</span>)) <span class="hljs-keyword">continue</span>;
<span class="hljs-keyword">const</span> event = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(line.<span class="hljs-title function_">slice</span>(<span class="hljs-number">6</span>));
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'meta'</span>)
<span class="hljs-title function_">setState</span>(<span class="hljs-function"><span class="hljs-params">s</span> =></span> ({ ...s, <span class="hljs-attr">meta</span>: event.<span class="hljs-property">meta</span> ?? <span class="hljs-literal">null</span>, <span class="hljs-attr">subtitles</span>: event.<span class="hljs-property">subtitles</span> ?? [] }));
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'source'</span>)
<span class="hljs-title function_">setState</span>(<span class="hljs-function"><span class="hljs-params">s</span> =></span> ({ ...s, <span class="hljs-attr">sources</span>: [...s.<span class="hljs-property">sources</span>, event.<span class="hljs-property">source</span>] }));
<span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">'done'</span>)
<span class="hljs-title function_">setState</span>(<span class="hljs-function"><span class="hljs-params">s</span> =></span> ({ ...s, <span class="hljs-attr">loading</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">done</span>: <span class="hljs-literal">true</span> }));
}
}
} <span class="hljs-keyword">catch</span> (<span class="hljs-attr">err</span>: <span class="hljs-built_in">any</span>) {
<span class="hljs-keyword">if</span> (err.<span class="hljs-property">name</span> !== <span class="hljs-string">'AbortError'</span>)
<span class="hljs-title function_">setState</span>(<span class="hljs-function"><span class="hljs-params">s</span> =></span> ({ ...s, <span class="hljs-attr">loading</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: err.<span class="hljs-property">message</span> }));
}
})();
<span class="hljs-keyword">return</span> <span class="hljs-function">() =></span> controller.<span class="hljs-title function_">abort</span>();
}, [tmdbId, season, episode]);
<span class="hljs-keyword">return</span> state;
}
</code></pre>
<h3 id="attachsource-utility"><code>attachSource</code> Utility</h3>
<p>Centralises HLS vs MP4 detection so every component shares one path.</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">import</span> <span class="hljs-title class_">Hls</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'hls.js'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">isMp4</span>(<span class="hljs-params"><span class="hljs-attr">url</span>: <span class="hljs-built_in">string</span></span>): <span class="hljs-built_in">boolean</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> inner = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(url).<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">'url'</span>) ?? url;
<span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(inner);
} <span class="hljs-keyword">catch</span> { <span class="hljs-keyword">return</span> <span class="hljs-regexp">/\.(mp4|mkv)(\?|$)/i</span>.<span class="hljs-title function_">test</span>(url); }
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">AttachResult</span> {
<span class="hljs-attr">hls</span>: <span class="hljs-title class_">Hls</span> | <span class="hljs-literal">null</span>;
<span class="hljs-attr">type</span>: <span class="hljs-string">'hls'</span> | <span class="hljs-string">'mp4'</span> | <span class="hljs-string">'native-hls'</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">attachSource</span>(<span class="hljs-params">
<span class="hljs-attr">videoEl</span>: <span class="hljs-title class_">HTMLVideoElement</span>,
<span class="hljs-attr">url</span>: <span class="hljs-built_in">string</span>,
<span class="hljs-attr">prevHls</span>?: <span class="hljs-title class_">Hls</span> | <span class="hljs-literal">null</span>,
<span class="hljs-attr">onLevels</span>?: (levels: Hls.Level[]) => <span class="hljs-built_in">void</span>,
<span class="hljs-attr">onError</span>?: () => <span class="hljs-built_in">void</span>,
</span>): <span class="hljs-title class_">AttachResult</span> {
prevHls?.<span class="hljs-title function_">destroy</span>();
<span class="hljs-keyword">if</span> (<span class="hljs-title function_">isMp4</span>(url)) {
videoEl.<span class="hljs-property">src</span> = url;
videoEl.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
<span class="hljs-keyword">return</span> { <span class="hljs-attr">hls</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">'mp4'</span> };
}
<span class="hljs-keyword">if</span> (<span class="hljs-title class_">Hls</span>.<span class="hljs-title function_">isSupported</span>()) {
<span class="hljs-keyword">const</span> hls = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Hls</span>({ <span class="hljs-attr">startLevel</span>: -<span class="hljs-number">1</span> });
hls.<span class="hljs-title function_">loadSource</span>(url);
hls.<span class="hljs-title function_">attachMedia</span>(videoEl);
hls.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">MANIFEST_PARSED</span>, <span class="hljs-function">(<span class="hljs-params">_, data</span>) =></span> {
videoEl.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
onLevels?.(data.<span class="hljs-property">levels</span>);
});
hls.<span class="hljs-title function_">on</span>(<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Events</span>.<span class="hljs-property">ERROR</span>, <span class="hljs-function">(<span class="hljs-params">_, err</span>) =></span> {
<span class="hljs-keyword">if</span> (err.<span class="hljs-property">fatal</span>) onError?.();
});
<span class="hljs-keyword">return</span> { hls, <span class="hljs-attr">type</span>: <span class="hljs-string">'hls'</span> };
}
<span class="hljs-keyword">if</span> (videoEl.<span class="hljs-title function_">canPlayType</span>(<span class="hljs-string">'application/vnd.apple.mpegurl'</span>)) {
videoEl.<span class="hljs-property">src</span> = url;
videoEl.<span class="hljs-title function_">play</span>().<span class="hljs-title function_">catch</span>(<span class="hljs-function">() =></span> {});
<span class="hljs-keyword">return</span> { <span class="hljs-attr">hls</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">'native-hls'</span> };
}
<span class="hljs-keyword">return</span> { <span class="hljs-attr">hls</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">'mp4'</span> };
}
</code></pre>
<h3 id="streamplayer-component"><code>StreamPlayer</code> Component</h3>
<p>Full-featured player: auto-starts on first source, queues fallbacks, source switcher, quality selector,
subtitle tracks. Works for movies and TV.</p>
<pre><code class="hljs language-tsx"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title class_">Hls</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'hls.js'</span>;
<span class="hljs-keyword">import</span> { useStream, <span class="hljs-title class_">StreamSource</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useStream'</span>;
<span class="hljs-keyword">import</span> { attachSource, isMp4 } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/attachSource'</span>;
<span class="hljs-keyword">interface</span> <span class="hljs-title class_">Props</span> {
<span class="hljs-attr">tmdbId</span>: <span class="hljs-built_in">number</span>;
<span class="hljs-attr">season</span>?: <span class="hljs-built_in">number</span>;
<span class="hljs-attr">episode</span>?: <span class="hljs-built_in">number</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">StreamPlayer</span>(<span class="hljs-params">{ tmdbId, season, episode }: <span class="hljs-title class_">Props</span></span>) {
<span class="hljs-keyword">const</span> videoRef = useRef&#x3C;<span class="hljs-title class_">HTMLVideoElement</span>>(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> hlsRef = useRef&#x3C;<span class="hljs-title class_">Hls</span> | <span class="hljs-literal">null</span>>(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> queueRef = useRef&#x3C;<span class="hljs-title class_">StreamSource</span>[]>([]);
<span class="hljs-keyword">const</span> startedRef = <span class="hljs-title function_">useRef</span>(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> activeRef = useRef&#x3C;<span class="hljs-built_in">string</span>>(<span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> { sources, subtitles, meta, loading, done, error } = <span class="hljs-title function_">useStream</span>(tmdbId, season, episode);
<span class="hljs-keyword">const</span> [activeSource, setActiveSource] = useState&#x3C;<span class="hljs-built_in">string</span>>(<span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> [levels, setLevels] = useState&#x3C;<span class="hljs-title class_">Hls</span>.<span class="hljs-property">Level</span>[]>([]);
<span class="hljs-keyword">const</span> [quality, setQuality] = <span class="hljs-title function_">useState</span>(-<span class="hljs-number">1</span>);
<span class="hljs-keyword">const</span> [status, setStatus] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">'Connecting…'</span>);
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
startedRef.<span class="hljs-property">current</span> = <span class="hljs-literal">false</span>;
queueRef.<span class="hljs-property">current</span> = [];
activeRef.<span class="hljs-property">current</span> = <span class="hljs-string">''</span>;
<span class="hljs-title function_">setActiveSource</span>(<span class="hljs-string">''</span>);
<span class="hljs-title function_">setLevels</span>([]);
<span class="hljs-title function_">setQuality</span>(-<span class="hljs-number">1</span>);
<span class="hljs-title function_">setStatus</span>(<span class="hljs-string">'Connecting…'</span>);
}, [tmdbId, season, episode]);
<span class="hljs-keyword">function</span> <span class="hljs-title function_">playSource</span>(<span class="hljs-params"><span class="hljs-attr">src</span>: <span class="hljs-title class_">StreamSource</span></span>) {
<span class="hljs-keyword">if</span> (!videoRef.<span class="hljs-property">current</span>) <span class="hljs-keyword">return</span>;
activeRef.<span class="hljs-property">current</span> = src.<span class="hljs-property">url</span>;
<span class="hljs-title function_">setActiveSource</span>(src.<span class="hljs-property">source</span>);
<span class="hljs-title function_">setLevels</span>([]);
<span class="hljs-title function_">setQuality</span>(-<span class="hljs-number">1</span>);
<span class="hljs-title function_">setStatus</span>(<span class="hljs-string">`Loading <span class="hljs-subst">${src.label}</span>…`</span>);
<span class="hljs-keyword">const</span> { hls } = <span class="hljs-title function_">attachSource</span>(
videoRef.<span class="hljs-property">current</span>,
src.<span class="hljs-property">url</span>,
hlsRef.<span class="hljs-property">current</span>,
<span class="hljs-function">(<span class="hljs-params">newLevels</span>) =></span> {
<span class="hljs-title function_">setLevels</span>(newLevels);
<span class="hljs-title function_">setStatus</span>(<span class="hljs-string">`Playing via <span class="hljs-subst">${src.label}</span><span class="hljs-subst">${isMp4(src.url) ? <span class="hljs-string">' · MP4'</span> : <span class="hljs-string">' · HLS'</span>}</span>`</span>);
},
<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> next = queueRef.<span class="hljs-property">current</span>.<span class="hljs-title function_">shift</span>();
<span class="hljs-keyword">if</span> (next) <span class="hljs-title function_">playSource</span>(next);
<span class="hljs-keyword">else</span> <span class="hljs-title function_">setStatus</span>(<span class="hljs-string">'All sources failed.'</span>);
},
);
hlsRef.<span class="hljs-property">current</span> = hls;
<span class="hljs-keyword">if</span> (<span class="hljs-title function_">isMp4</span>(src.<span class="hljs-property">url</span>))
<span class="hljs-title function_">setStatus</span>(<span class="hljs-string">`Playing via <span class="hljs-subst">${src.label}</span> · MP4`</span>);
}
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (!sources.<span class="hljs-property">length</span>) <span class="hljs-keyword">return</span>;
<span class="hljs-keyword">const</span> latest = sources[sources.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>];
<span class="hljs-keyword">if</span> (!startedRef.<span class="hljs-property">current</span>) {
startedRef.<span class="hljs-property">current</span> = <span class="hljs-literal">true</span>;
<span class="hljs-title function_">playSource</span>(latest);
} <span class="hljs-keyword">else</span> {
queueRef.<span class="hljs-property">current</span>.<span class="hljs-title function_">push</span>(latest);
}
}, [sources]);
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (hlsRef.<span class="hljs-property">current</span>) hlsRef.<span class="hljs-property">current</span>.<span class="hljs-property">currentLevel</span> = quality;
}, [quality]);
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> <span class="hljs-function">() =></span> { hlsRef.<span class="hljs-property">current</span>?.<span class="hljs-title function_">destroy</span>(); }, []);
<span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1rem</span>', <span class="hljs-attr">color:</span> '#<span class="hljs-attr">f87171</span>' }}></span>
Stream error: {error}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>
);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">width:</span> '<span class="hljs-attr">100</span>%' }}></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">video</span>
<span class="hljs-attr">ref</span>=<span class="hljs-string">{videoRef}</span>
<span class="hljs-attr">controls</span>
<span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">width:</span> '<span class="hljs-attr">100</span>%', <span class="hljs-attr">background:</span> '#<span class="hljs-attr">000</span>', <span class="hljs-attr">display:</span> '<span class="hljs-attr">block</span>', <span class="hljs-attr">borderRadius:</span> <span class="hljs-attr">6</span> }}
></span>
{subtitles.map(sub => (
<span class="hljs-tag">&#x3C;<span class="hljs-name">track</span>
<span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">sub.label</span>}<span class="hljs-attr">-</span>${<span class="hljs-attr">sub.source</span>}`}
<span class="hljs-attr">kind</span>=<span class="hljs-string">"subtitles"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">{sub.label}</span>
<span class="hljs-attr">src</span>=<span class="hljs-string">{sub.file}</span>
<span class="hljs-attr">srcLang</span>=<span class="hljs-string">{sub.label.slice(0,</span> <span class="hljs-attr">2</span>)<span class="hljs-attr">.toLowerCase</span>()}
/></span>
))}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">video</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> '<span class="hljs-attr">0.75rem</span>', <span class="hljs-attr">color:</span> '#<span class="hljs-attr">666</span>', <span class="hljs-attr">marginTop:</span> <span class="hljs-attr">4</span> }}></span>
{loading &#x26;&#x26; !sources.length ? 'Connecting to providers…' : status}
{done &#x26;&#x26; ` · ${sources.length} source${sources.length !== 1 ? 's' : ''} found`}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
{(sources.length > 0 || levels.length > 1) &#x26;&#x26; (
<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> '<span class="hljs-attr">flex</span>', <span class="hljs-attr">gap:</span> '<span class="hljs-attr">0.5rem</span>', <span class="hljs-attr">flexWrap:</span> '<span class="hljs-attr">wrap</span>', <span class="hljs-attr">marginTop:</span> '<span class="hljs-attr">0.5rem</span>', <span class="hljs-attr">alignItems:</span> '<span class="hljs-attr">center</span>' }}></span>
{levels.length > 1 &#x26;&#x26; (
<span class="hljs-tag">&#x3C;<span class="hljs-name">select</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{quality}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{e</span> =></span> setQuality(Number(e.target.value))}
style={{ fontSize: '0.8rem', padding: '0.3rem 0.5rem', borderRadius: 4,
background: '#1e1e1e', color: '#ccc', border: '1px solid #333' }}
>
<span class="hljs-tag">&#x3C;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{-1}</span>></span>Quality: Auto<span class="hljs-tag">&#x3C;/<span class="hljs-name">option</span>></span>
{levels.map((l, i) => (
<span class="hljs-tag">&#x3C;<span class="hljs-name">option</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{i}</span>></span>
{l.height ? `${l.height}p` : `Level ${i}`}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">option</span>></span>
))}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">select</span>></span>
)}
{sources.length > 1 &#x26;&#x26; sources.map(src => (
<span class="hljs-tag">&#x3C;<span class="hljs-name">button</span>
<span class="hljs-attr">key</span>=<span class="hljs-string">{src.source}</span>
<span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =></span> playSource(src)}
style={{
fontSize: '0.75rem', padding: '0.3rem 0.65rem', borderRadius: 4, cursor: 'pointer',
background: activeSource === src.source ? '#fff' : '#1e1e1e',
color: activeSource === src.source ? '#111' : '#aaa',
border: `1px solid ${activeSource === src.source ? '#fff' : '#333'}`,
}}
>
{src.label}
<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> '<span class="hljs-attr">0.6rem</span>', <span class="hljs-attr">marginLeft:</span> <span class="hljs-attr">4</span>, <span class="hljs-attr">opacity:</span> <span class="hljs-attr">0.6</span> }}></span>
{isMp4(src.url) ? 'MP4' : 'HLS'}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
))}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
)}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>
);
}
</code></pre>
<h3 id="episode-selector">Episode Selector</h3>
<p>Renders an episode list and mounts the player only when an episode is selected — no streams open until the
user picks something.</p>
<pre><code class="hljs language-tsx"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">StreamPlayer</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./StreamPlayer'</span>;
<span class="hljs-keyword">interface</span> <span class="hljs-title class_">Episode</span> {
<span class="hljs-attr">episode_number</span>: <span class="hljs-built_in">number</span>;
<span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>;
}
<span class="hljs-keyword">interface</span> <span class="hljs-title class_">Props</span> {
<span class="hljs-attr">seriesId</span>: <span class="hljs-built_in">number</span>;
<span class="hljs-attr">season</span>: <span class="hljs-built_in">number</span>;
<span class="hljs-attr">episodes</span>: <span class="hljs-title class_">Episode</span>[];
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">EpisodeSelector</span>(<span class="hljs-params">{ seriesId, season, episodes }: <span class="hljs-title class_">Props</span></span>) {
<span class="hljs-keyword">const</span> [selected, setSelected] = useState&#x3C;<span class="hljs-built_in">number</span> | <span class="hljs-literal">null</span>>(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> '<span class="hljs-attr">grid</span>', <span class="hljs-attr">gridTemplateColumns:</span> '<span class="hljs-attr">220px</span> <span class="hljs-attr">1fr</span>', <span class="hljs-attr">gap:</span> '<span class="hljs-attr">1rem</span>' }}></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">aside</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">overflowY:</span> '<span class="hljs-attr">auto</span>', <span class="hljs-attr">maxHeight:</span> <span class="hljs-attr">480</span>, <span class="hljs-attr">borderRight:</span> '<span class="hljs-attr">1px</span> <span class="hljs-attr">solid</span> #<span class="hljs-attr">222</span>', <span class="hljs-attr">paddingRight:</span> '<span class="hljs-attr">0.75rem</span>' }}></span>
{episodes.map(ep => (
<span class="hljs-tag">&#x3C;<span class="hljs-name">button</span>
<span class="hljs-attr">key</span>=<span class="hljs-string">{ep.episode_number}</span>
<span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =></span> setSelected(ep.episode_number)}
style={{
display: 'block', width: '100%', textAlign: 'left',
padding: '0.5rem 0.6rem', marginBottom: 2, borderRadius: 4, cursor: 'pointer',
background: selected === ep.episode_number ? '#fff' : 'transparent',
color: selected === ep.episode_number ? '#111' : '#aaa',
border: 'none', fontSize: '0.82rem',
}}
>
<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">opacity:</span> <span class="hljs-attr">0.5</span>, <span class="hljs-attr">marginRight:</span> <span class="hljs-attr">6</span> }}></span>E{ep.episode_number}<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
{ep.name}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
))}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">aside</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">section</span>></span>
{selected === null
? <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">color:</span> '#<span class="hljs-attr">555</span>', <span class="hljs-attr">paddingTop:</span> '<span class="hljs-attr">2rem</span>' }}></span>Select an episode to watch<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
: <span class="hljs-tag">&#x3C;<span class="hljs-name">StreamPlayer</span> <span class="hljs-attr">tmdbId</span>=<span class="hljs-string">{seriesId}</span> <span class="hljs-attr">season</span>=<span class="hljs-string">{season}</span> <span class="hljs-attr">episode</span>=<span class="hljs-string">{selected}</span> /></span>
}
<span class="hljs-tag">&#x3C;/<span class="hljs-name">section</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>
);
}
</code></pre>
<hr>
<h2 id="nextjs-app-router">Next.js App Router</h2>
<h3 id="movie-page">Movie Page</h3>
<pre><code class="hljs language-tsx"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">StreamPlayer</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/StreamPlayer'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">MoviePage</span>(<span class="hljs-params">{ params }: { params: { id: <span class="hljs-built_in">string</span> } }</span>) {
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">main</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">maxWidth:</span> <span class="hljs-attr">960</span>, <span class="hljs-attr">margin:</span> '<span class="hljs-attr">0</span> <span class="hljs-attr">auto</span>', <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1.5rem</span> <span class="hljs-attr">1rem</span>' }}></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">StreamPlayer</span> <span class="hljs-attr">tmdbId</span>=<span class="hljs-string">{Number(params.id)}</span> /></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">main</span>></span></span>
);
}
</code></pre>
<h3 id="tv-episode-page">TV Episode Page</h3>
<pre><code class="hljs language-tsx"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">StreamPlayer</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/StreamPlayer'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">EpisodePage</span>(<span class="hljs-params">{
params,
}: {
params: { id: <span class="hljs-built_in">string</span>; season: <span class="hljs-built_in">string</span>; episode: <span class="hljs-built_in">string</span> };
}</span>) {
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">main</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">maxWidth:</span> <span class="hljs-attr">960</span>, <span class="hljs-attr">margin:</span> '<span class="hljs-attr">0</span> <span class="hljs-attr">auto</span>', <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1.5rem</span> <span class="hljs-attr">1rem</span>' }}></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">StreamPlayer</span>
<span class="hljs-attr">tmdbId</span>=<span class="hljs-string">{Number(params.id)}</span>
<span class="hljs-attr">season</span>=<span class="hljs-string">{Number(params.season)}</span>
<span class="hljs-attr">episode</span>=<span class="hljs-string">{Number(params.episode)}</span>
/></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">main</span>></span></span>
);
}
</code></pre>
<h3 id="dynamic-hlsjs-import-ssr-safe">Dynamic HLS.js Import (SSR-safe)</h3>
<p>HLS.js accesses <code>window</code> and will crash during SSR. Either mark the component
<code>'use client'</code> (already done above) or use a dynamic import to exclude it from the server bundle
entirely:
</p>
<pre><code class="hljs language-tsx"><span class="hljs-keyword">import</span> dynamic <span class="hljs-keyword">from</span> <span class="hljs-string">'next/dynamic'</span>;
<span class="hljs-keyword">const</span> <span class="hljs-title class_">StreamPlayer</span> = <span class="hljs-title function_">dynamic</span>(
<span class="hljs-function">() =></span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'@/components/StreamPlayer'</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">m</span> =></span> m.<span class="hljs-property">StreamPlayer</span>),
{ <span class="hljs-attr">ssr</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">loading</span>: <span class="hljs-function">() =></span> <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">aspectRatio:</span> '<span class="hljs-attr">16</span>/<span class="hljs-attr">9</span>', <span class="hljs-attr">background:</span> '#<span class="hljs-attr">111</span>', <span class="hljs-attr">borderRadius:</span> <span class="hljs-attr">6</span> }} /></span></span> }
);
</code></pre>
<div class="callout callout-warning">
<div class="callout-content">
<p>Never import <code>hls.js</code> at the top level of a file that can run on the server. Always use
<code>'use client'</code> or a dynamic import with <code>ssr: false</code>.
</p>
</div>
</div>
<hr>
<h2 id="vue-3">Vue 3</h2>
<pre><code class="hljs language-vue">&#x3C;template>
&#x3C;div>
&#x3C;video ref="videoEl" controls style="width:100%; background:#000; border-radius:6px; display:block">
&#x3C;track
v-for="sub in subtitles"
:key="sub.label + sub.source"
kind="subtitles"
:label="sub.label"
:src="sub.file"
/>
&#x3C;/video>
&#x3C;div style="font-size:0.75rem; color:#666; margin-top:4px">{{ status }}&#x3C;/div>
&#x3C;div v-if="sources.length > 1" style="display:flex; gap:0.4rem; flex-wrap:wrap; margin-top:0.5rem">
&#x3C;button
v-for="src in sources"
:key="src.source"
@click="playSource(src)"
:style="{
fontSize: '0.75rem', padding: '0.3rem 0.65rem', borderRadius: '4px', cursor: 'pointer',
background: activeSource === src.source ? '#fff' : '#1e1e1e',
color: activeSource === src.source ? '#111' : '#aaa',
border: `1px solid ${activeSource === src.source ? '#fff' : '#333'}`,
}"
>
{{ src.label }}
&#x3C;span style="font-size:0.6rem; margin-left:4px; opacity:0.6">
{{ isMp4(src.url) ? 'MP4' : 'HLS' }}
&#x3C;/span>
&#x3C;/button>
&#x3C;/div>
&#x3C;/div>
&#x3C;/template>
&#x3C;script setup lang="ts">
import { ref, watch, onUnmounted } from 'vue';
import Hls from 'hls.js';
const props = defineProps&#x3C;{
tmdbId: number;
season?: number;
episode?: number;
}>();
const BASE = 'https://api.vyla.cc';
interface Source { source: string; label: string; url: string; }
interface Subtitle { label: string; file: string; type: string; source: string; }
const videoEl = ref&#x3C;HTMLVideoElement | null>(null);
const sources = ref&#x3C;Source[]>([]);
const subtitles = ref&#x3C;Subtitle[]>([]);
const status = ref('Connecting…');
const activeSource = ref('');
let hlsInstance: Hls | null = null;
let abortController: AbortController | null = null;
let started = false;
let queue: Source[] = [];
function isMp4(url: string): boolean {
try {
const inner = new URL(url).searchParams.get('url') ?? url;
return /\.(mp4|mkv)(\?|$)/i.test(inner);
} catch { return /\.(mp4|mkv)(\?|$)/i.test(url); }
}
function playSource(src: Source) {
if (!videoEl.value) return;
hlsInstance?.destroy();
hlsInstance = null;
activeSource.value = src.source;
if (isMp4(src.url)) {
videoEl.value.src = src.url;
videoEl.value.play().catch(() => {});
status.value = `Playing ${src.label} · MP4`;
return;
}
if (Hls.isSupported()) {
hlsInstance = new Hls({ startLevel: -1 });
hlsInstance.loadSource(src.url);
hlsInstance.attachMedia(videoEl.value);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
videoEl.value?.play().catch(() => {});
status.value = `Playing ${src.label} · HLS`;
});
hlsInstance.on(Hls.Events.ERROR, (_, err) => {
if (err.fatal) {
const next = queue.shift();
if (next) playSource(next);
else status.value = 'All sources failed.';
}
});
return;
}
if (videoEl.value.canPlayType('application/vnd.apple.mpegurl')) {
videoEl.value.src = src.url;
videoEl.value.play().catch(() => {});
status.value = `Playing ${src.label} · HLS`;
}
}
async function loadStream() {
abortController?.abort();
abortController = new AbortController();
sources.value = [];
subtitles.value = [];
status.value = 'Connecting…';
started = false;
queue = [];
const url = props.season &#x26;&#x26; props.episode
? `${BASE}/tv?id=${props.tmdbId}&#x26;season=${props.season}&#x26;episode=${props.episode}`
: `${BASE}/movie?id=${props.tmdbId}`;
try {
const res = await fetch(url, {
signal: abortController.signal,
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop()!;
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
if (event.type === 'meta') {
subtitles.value = event.subtitles ?? [];
status.value = `Loading ${event.meta?.title ?? event.meta?.name ?? ''}…`;
}
if (event.type === 'source') {
sources.value = [...sources.value, event.source];
if (!started) { started = true; playSource(event.source); }
else queue.push(event.source);
}
if (event.type === 'done' &#x26;&#x26; !started) {
status.value = 'No sources available.';
}
}
}
} catch (err: any) {
if (err.name !== 'AbortError') status.value = `Error: ${err.message}`;
}
}
watch(() => [props.tmdbId, props.season, props.episode], loadStream, { immediate: true });
onUnmounted(() => { hlsInstance?.destroy(); abortController?.abort(); });
&#x3C;/script>
</code></pre>
<hr>
<h2 id="android-kotlin--exoplayer">Android (Kotlin + ExoPlayer)</h2>
<p>ExoPlayer handles both HLS and MP4 natively — just pass the URL and it auto-detects the type.</p>
<pre><code class="hljs language-kotlin"><span class="hljs-keyword">import</span> androidx.media3.common.MediaItem
<span class="hljs-keyword">import</span> androidx.media3.exoplayer.ExoPlayer
<span class="hljs-keyword">import</span> kotlinx.coroutines.*
<span class="hljs-keyword">import</span> org.json.JSONObject
<span class="hljs-keyword">import</span> java.io.BufferedReader
<span class="hljs-keyword">import</span> java.io.InputStreamReader
<span class="hljs-keyword">import</span> java.net.HttpURLConnection
<span class="hljs-keyword">import</span> java.net.URL
<span class="hljs-keyword">class</span> <span class="hljs-title class_">StreamActivity</span> : <span class="hljs-type">AppCompatActivity</span>() {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> BASE = <span class="hljs-string">"https://api.vyla.cc"</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> player: ExoPlayer
<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> scope = CoroutineScope(Dispatchers.Main + Job())
<span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
<span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
player = ExoPlayer.Builder(<span class="hljs-keyword">this</span>).build()
binding.playerView.player = player
<span class="hljs-keyword">val</span> tmdbId = intent.getIntExtra(<span class="hljs-string">"tmdb_id"</span>, <span class="hljs-number">550</span>)
loadStream(tmdbId)
}
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadStream</span><span class="hljs-params">(tmdbId: <span class="hljs-type">Int</span>, season: <span class="hljs-type">Int</span>? = <span class="hljs-literal">null</span>, episode: <span class="hljs-type">Int</span>? = <span class="hljs-literal">null</span>)</span></span> {
scope.launch {
<span class="hljs-keyword">val</span> streamUrl = <span class="hljs-keyword">if</span> (season != <span class="hljs-literal">null</span> &#x26;&#x26; episode != <span class="hljs-literal">null</span>)
<span class="hljs-string">"<span class="hljs-variable">$BASE</span>/tv?id=<span class="hljs-variable">$tmdbId</span>&#x26;season=<span class="hljs-variable">$season</span>&#x26;episode=<span class="hljs-variable">$episode</span>"</span>
<span class="hljs-keyword">else</span>
<span class="hljs-string">"<span class="hljs-variable">$BASE</span>/movie?id=<span class="hljs-variable">$tmdbId</span>"</span>
withContext(Dispatchers.IO) {
<span class="hljs-keyword">val</span> conn = URL(streamUrl).openConnection() <span class="hljs-keyword">as</span> HttpURLConnection
<span class="hljs-keyword">val</span> reader = BufferedReader(InputStreamReader(conn.inputStream))
reader.forEachLine { line ->
<span class="hljs-keyword">if</span> (!line.startsWith(<span class="hljs-string">"data: "</span>)) <span class="hljs-keyword">return</span><span class="hljs-symbol">@forEachLine</span>
<span class="hljs-keyword">val</span> json = JSONObject(line.substring(<span class="hljs-number">6</span>))
<span class="hljs-keyword">when</span> (json.getString(<span class="hljs-string">"type"</span>)) {
<span class="hljs-string">"source"</span> -> {
<span class="hljs-keyword">val</span> source = json.getJSONObject(<span class="hljs-string">"source"</span>)
<span class="hljs-keyword">val</span> url = source.getString(<span class="hljs-string">"url"</span>)
withContext(Dispatchers.Main) {
<span class="hljs-keyword">if</span> (!player.isPlaying) {
player.setMediaItem(MediaItem.fromUri(url))
player.prepare()
player.play()
}
}
}
}
}
}
}
}
<span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onDestroy</span><span class="hljs-params">()</span></span> {
<span class="hljs-keyword">super</span>.onDestroy()
scope.cancel()
player.release()
}
}
</code></pre>
<div class="callout callout-tip">
<div class="callout-content">
<p>ExoPlayer resolves HLS and MP4 automatically from the <code>Content-Type</code> returned by the proxy. No
manual type detection needed on Android.</p>
</div>
</div>
<hr>
<h2 id="ios-swift--avplayer">iOS (Swift + AVPlayer)</h2>
<p>AVPlayer handles both HLS and MP4 natively. Same SSE parsing, different player attachment.</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">import</span> UIKit
<span class="hljs-keyword">import</span> AVKit
<span class="hljs-keyword">class</span> <span class="hljs-title class_">StreamViewController</span>: <span class="hljs-title class_ inherited__">UIViewController</span> {
<span class="hljs-keyword">let</span> <span class="hljs-type">BASE</span> <span class="hljs-operator">=</span> <span class="hljs-string">"https://api.vyla.cc"</span>
<span class="hljs-keyword">var</span> player: <span class="hljs-type">AVPlayer</span>?
<span class="hljs-keyword">var</span> playerLayer: <span class="hljs-type">AVPlayerLayer</span>?
<span class="hljs-keyword">var</span> streamTask: <span class="hljs-type">URLSessionDataTask</span>?
<span class="hljs-keyword">var</span> started <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>
<span class="hljs-keyword">override</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">viewDidLoad</span>() {
<span class="hljs-keyword">super</span>.viewDidLoad()
loadStream(tmdbId: <span class="hljs-number">550</span>)
}
<span class="hljs-keyword">func</span> <span class="hljs-title function_">loadStream</span>(<span class="hljs-params">tmdbId</span>: <span class="hljs-type">Int</span>, <span class="hljs-params">season</span>: <span class="hljs-type">Int</span>? <span class="hljs-operator">=</span> <span class="hljs-literal">nil</span>, <span class="hljs-params">episode</span>: <span class="hljs-type">Int</span>? <span class="hljs-operator">=</span> <span class="hljs-literal">nil</span>) {
<span class="hljs-keyword">let</span> path: <span class="hljs-type">String</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> s <span class="hljs-operator">=</span> season, <span class="hljs-keyword">let</span> e <span class="hljs-operator">=</span> episode {
path <span class="hljs-operator">=</span> <span class="hljs-string">"<span class="hljs-subst">\(<span class="hljs-keyword">self</span>.BASE)</span>/tv?id=<span class="hljs-subst">\(tmdbId)</span>&#x26;season=<span class="hljs-subst">\(s)</span>&#x26;episode=<span class="hljs-subst">\(e)</span>"</span>
} <span class="hljs-keyword">else</span> {
path <span class="hljs-operator">=</span> <span class="hljs-string">"<span class="hljs-subst">\(<span class="hljs-keyword">self</span>.BASE)</span>/movie?id=<span class="hljs-subst">\(tmdbId)</span>"</span>
}
<span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> url <span class="hljs-operator">=</span> <span class="hljs-type">URL</span>(string: path) <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
<span class="hljs-keyword">var</span> request <span class="hljs-operator">=</span> <span class="hljs-type">URLRequest</span>(url: url)
<span class="hljs-keyword">class</span> <span class="hljs-title class_">SSEDelegate</span>: <span class="hljs-title class_ inherited__">NSObject</span>, <span class="hljs-title class_ inherited__">URLSessionDataDelegate</span> {
<span class="hljs-keyword">var</span> buffer <span class="hljs-operator">=</span> <span class="hljs-string">""</span>
<span class="hljs-keyword">var</span> onSource: (<span class="hljs-type">String</span>) -> <span class="hljs-type">Void</span>
<span class="hljs-keyword">init</span>(<span class="hljs-keyword">_</span> <span class="hljs-params">onSource</span>: <span class="hljs-keyword">@escaping</span> (<span class="hljs-type">String</span>) -> <span class="hljs-type">Void</span>) { <span class="hljs-keyword">self</span>.onSource <span class="hljs-operator">=</span> onSource }
<span class="hljs-keyword">func</span> <span class="hljs-title function_">urlSession</span>(<span class="hljs-keyword">_</span> <span class="hljs-params">session</span>: <span class="hljs-type">URLSession</span>, <span class="hljs-params">dataTask</span>: <span class="hljs-type">URLSessionDataTask</span>, <span class="hljs-params">didReceive</span> <span class="hljs-params">data</span>: <span class="hljs-type">Data</span>) {
<span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> text <span class="hljs-operator">=</span> <span class="hljs-type">String</span>(data: data, encoding: .utf8) <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
buffer <span class="hljs-operator">+=</span> text
<span class="hljs-keyword">while</span> <span class="hljs-keyword">let</span> range <span class="hljs-operator">=</span> buffer.range(of: <span class="hljs-string">"<span class="hljs-subst">\n</span>"</span>) {
<span class="hljs-keyword">let</span> line <span class="hljs-operator">=</span> <span class="hljs-type">String</span>(buffer[<span class="hljs-operator">..&#x3C;</span>range.lowerBound])
buffer.removeSubrange(<span class="hljs-operator">..&#x3C;</span>range.upperBound)
<span class="hljs-keyword">guard</span> line.hasPrefix(<span class="hljs-string">"data: "</span>),
<span class="hljs-keyword">let</span> jsonData <span class="hljs-operator">=</span> line.dropFirst(<span class="hljs-number">6</span>).data(using: .utf8),
<span class="hljs-keyword">let</span> json <span class="hljs-operator">=</span> <span class="hljs-keyword">try?</span> <span class="hljs-type">JSONSerialization</span>.jsonObject(with: jsonData) <span class="hljs-keyword">as?</span> [<span class="hljs-type">String</span>: <span class="hljs-keyword">Any</span>],
<span class="hljs-keyword">let</span> type <span class="hljs-operator">=</span> json[<span class="hljs-string">"type"</span>] <span class="hljs-keyword">as?</span> <span class="hljs-type">String</span> <span class="hljs-keyword">else</span> { <span class="hljs-keyword">continue</span> }
<span class="hljs-keyword">if</span> type <span class="hljs-operator">==</span> <span class="hljs-string">"source"</span>,
<span class="hljs-keyword">let</span> source <span class="hljs-operator">=</span> json[<span class="hljs-string">"source"</span>] <span class="hljs-keyword">as?</span> [<span class="hljs-type">String</span>: <span class="hljs-keyword">Any</span>],
<span class="hljs-keyword">let</span> streamUrl <span class="hljs-operator">=</span> source[<span class="hljs-string">"url"</span>] <span class="hljs-keyword">as?</span> <span class="hljs-type">String</span> {
onSource(streamUrl)
}
}
}
}
<span class="hljs-keyword">let</span> delegate <span class="hljs-operator">=</span> <span class="hljs-type">SSEDelegate</span> { [<span class="hljs-keyword">weak</span> <span class="hljs-keyword">self</span>] streamUrl <span class="hljs-keyword">in</span>
<span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> <span class="hljs-keyword">self</span>, <span class="hljs-operator">!</span><span class="hljs-keyword">self</span>.started <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
<span class="hljs-keyword">self</span>.started <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
<span class="hljs-type">DispatchQueue</span>.main.async {
<span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> url <span class="hljs-operator">=</span> <span class="hljs-type">URL</span>(string: streamUrl) <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
<span class="hljs-keyword">let</span> player <span class="hljs-operator">=</span> <span class="hljs-type">AVPlayer</span>(url: url)
<span class="hljs-keyword">self</span>.player <span class="hljs-operator">=</span> player
<span class="hljs-keyword">let</span> layer <span class="hljs-operator">=</span> <span class="hljs-type">AVPlayerLayer</span>(player: player)
layer.frame <span class="hljs-operator">=</span> <span class="hljs-keyword">self</span>.view.bounds
layer.videoGravity <span class="hljs-operator">=</span> .resizeAspect
<span class="hljs-keyword">self</span>.view.layer.addSublayer(layer)
<span class="hljs-keyword">self</span>.playerLayer <span class="hljs-operator">=</span> layer
player.play()
}
}
<span class="hljs-keyword">let</span> session <span class="hljs-operator">=</span> <span class="hljs-type">URLSession</span>(configuration: .default, delegate: delegate, delegateQueue: <span class="hljs-literal">nil</span>)
<span class="hljs-keyword">self</span>.streamTask <span class="hljs-operator">=</span> session.dataTask(with: request)
<span class="hljs-keyword">self</span>.streamTask<span class="hljs-operator">?</span>.resume()
}
}
<span class="hljs-keyword">override</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">viewWillDisappear</span>(<span class="hljs-keyword">_</span> <span class="hljs-params">animated</span>: <span class="hljs-type">Bool</span>) {
<span class="hljs-keyword">super</span>.viewWillDisappear(animated)
streamTask<span class="hljs-operator">?</span>.cancel()
player<span class="hljs-operator">?</span>.pause()
}
}
</code></pre>
<div class="callout callout-tip">
<div class="callout-content">
<p>AVPlayer resolves HLS via <code>application/vnd.apple.mpegurl</code> and MP4 via <code>video/mp4</code>
content-type returned by the proxy — no manual detection needed on iOS either.</p>
</div>
</div>
<hr>
<h2 id="python-mpv">Python (mpv)</h2>
<p>For desktop apps or scripts. <code>mpv</code> handles HLS and MP4 natively via its own demuxer.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> subprocess
BASE = <span class="hljs-string">'https://api.vyla.cc'</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">stream</span>(<span class="hljs-params">tmdb_id: <span class="hljs-built_in">int</span>, season: <span class="hljs-built_in">int</span> = <span class="hljs-literal">None</span>, episode: <span class="hljs-built_in">int</span> = <span class="hljs-literal">None</span></span>):
url = (
<span class="hljs-string">f'<span class="hljs-subst">{BASE}</span>/tv?id=<span class="hljs-subst">{tmdb_id}</span>&#x26;season=<span class="hljs-subst">{season}</span>&#x26;episode=<span class="hljs-subst">{episode}</span>'</span>
<span class="hljs-keyword">if</span> season <span class="hljs-keyword">and</span> episode <span class="hljs-keyword">else</span>
<span class="hljs-string">f'<span class="hljs-subst">{BASE}</span>/movie?id=<span class="hljs-subst">{tmdb_id}</span>'</span>
)
<span class="hljs-keyword">with</span> requests.get(url, stream=<span class="hljs-literal">True</span>) <span class="hljs-keyword">as</span> res:
<span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> res.iter_lines():
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> line <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> line.startswith(<span class="hljs-string">b'data: '</span>):
<span class="hljs-keyword">continue</span>
event = json.loads(line[<span class="hljs-number">6</span>:])
<span class="hljs-keyword">if</span> event[<span class="hljs-string">'type'</span>] == <span class="hljs-string">'source'</span>:
stream_url = event[<span class="hljs-string">'source'</span>][<span class="hljs-string">'url'</span>]
label = event[<span class="hljs-string">'source'</span>][<span class="hljs-string">'label'</span>]
<span class="hljs-built_in">print</span>(<span class="hljs-string">f'Playing via <span class="hljs-subst">{label}</span>: <span class="hljs-subst">{stream_url}</span>'</span>)
subprocess.run([<span class="hljs-string">'mpv'</span>, stream_url])
<span class="hljs-keyword">return</span>
<span class="hljs-keyword">if</span> event[<span class="hljs-string">'type'</span>] == <span class="hljs-string">'done'</span>:
<span class="hljs-built_in">print</span>(<span class="hljs-string">'No sources available.'</span>)
stream(<span class="hljs-number">550</span>)
</code></pre>
</main>
<aside id="toc">
<p class="toc-title">On this page</p>
<ul>
<li><a href="#source-type-detection">Source Type Detection</a></li>
<li><a href="#installation">Installation</a></li>
<li><a href="#how-the-stream-works">How the Stream Works</a></li>
<li><a href="#minimal-html-player">Minimal HTML Player</a></li>
<li><a href="#html-player--full-featured">HTML Player — Full Featured</a></li>
<li><a href="#react">React</a></li>
<li><a href="#nextjs-app-router">Next.js App Router</a></li>
<li><a href="#vue-3">Vue 3</a></li>
<li><a href="#android-kotlin--exoplayer">Android (Kotlin + ExoPlayer)</a></li>
<li><a href="#ios-swift--avplayer">iOS (Swift + AVPlayer)</a></li>
<li><a href="#python-mpv">Python (mpv)</a></li>
</ul>
</aside>
</div>
</div>
<footer id="site-footer">
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener">Discord</a>
<a href="https://gitlab.com/vyla-entertainment/" target="_blank" rel="noopener">GitLab</a>
</footer>
<script src="/assets/app.js"></script>
</body>
</html>