Sync project files

This commit is contained in:
2026-08-12 19:36:39 -07:00
commit 06f7e76d94
36 changed files with 1456 additions and 0 deletions

33
.github/workflows/vps-deploy.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Deploy to VPS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VPS_HOST }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/live-api-streampk
git fetch origin main
git reset --hard origin/main
git clean -fd
npm install
chown -R www-data:www-data /var/www/live-api-streampk
chmod -R 755 /var/www/live-api-streampk
pm2 restart live-api-streampk || pm2 start server.js --name live-api-streampk
pm2 save

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
package-lock.json

13
DOCKERFILE Normal file
View File

@@ -0,0 +1,13 @@
FROM node:22-slim
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 5000
ENV PORT=5000
CMD ["npm", "start"]

94
DOCUMENTATION/API.md Normal file
View File

@@ -0,0 +1,94 @@
# API Reference
All endpoints are relative to the server root (e.g. `http://localhost:5000`).
---
## `GET /api/stream/:source/:id`
Returns a JSON array of available streams for a given match.
**URL parameters**
- `source` – source name (`admin`, `echo`, `golf`, etc.)
- `id` – match slug (e.g. `ppv-brooklyn-nets-vs-milwaukee-bucks`)
**Example request**
```
GET /api/stream/admin/ppv-brooklyn-nets-vs-milwaukee-bucks
```
**Example response**
```json
[
{
"id": "ppv-brooklyn-nets-vs-milwaukee-bucks",
"streamNo": 1,
"language": "English - ESPN+",
"hd": true,
"streamUrl": "http://localhost:5000/api/m3u8-proxy?url=https%3A%2F%2Flb14.strmd.st%2Fsecure%2F...%2Fplaylist.m3u8",
"source": "admin",
"viewers": 14
}
]
```
**Fields**
| Field | Type | Description |
|------------|---------|-------------|
| `id` | string | Match identifier |
| `streamNo` | number | Stream number (used internally) |
| `language` | string | Human‑readable label |
| `hd` | boolean | Whether the stream is high‑definition |
| `streamUrl`| string | **Proxied** playlist URL – use this for playback |
| `source` | string | Source name |
| `viewers` | number | Approximate viewer count (may be `0`) |
> The endpoint may also accept a full watch URL or embed URL if your router supports it.
---
## `GET /api/m3u8-proxy`
Serves the HLS playlist or a segment.
The server automatically detects whether the response is a playlist or a segment and handles it accordingly.
**Query parameters**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `url` | ✅ | The upstream HLS URL (playlist or segment). |
| `referer` | ❌ | Override the `Referer` header sent to upstream. If omitted, `EMBED_DOMAIN` is used. |
**Behaviour**
- If the upstream response is an M3U8 playlist (contains `#EXTM3U`), it rewrites all media URLs and `URI` attributes to point back to `/api/m3u8-proxy`.
- If the response is an MPEG‑TS segment, it strips the PNG wrapper (if present) and returns raw TS data.
**Response headers**
- `Content-Type: application/vnd.apple.mpegurl` or `video/mp2t`
- `Access-Control-Allow-Origin: *`
- `Cache-Control: no-cache`
**Example**
```
GET /api/m3u8-proxy?url=https://lb14.strmd.st/secure/.../playlist.m3u8&referer=https://embed.st/
```
---
## `GET /api/segment-proxy` *(if mounted separately)*
Some implementations separate segment proxying from playlist proxying.
This endpoint behaves exactly like `/api/m3u8-proxy` but is dedicated to segments and always returns `video/mp2t`.
**Same query parameters as above.**
---
## Error Responses
All endpoints return appropriate HTTP status codes:
- `400` – Missing required parameter.
- `502` – Upstream fetch failed (non‑2xx status, network error, or invalid response).
- `500` – Internal server error (e.g., resolver failure).
Error responses are plain text with a descriptive message.

34
DOCUMENTATION/README.md Normal file
View File

@@ -0,0 +1,34 @@
# HLS Stream Proxy Server
A Node.js server that resolves and proxies HLS streams from **streamed.pk / embed.st** (and **golf** embeds) so they can be played in any browser or media player without referer or CORS issues.
## Features
- Resolves `admin`/`echo` streams using the embed.st handshake.
- Resolves `golf` streams via a third‑party embed chain.
- Proxies HLS playlists and segments with correct `Referer`/`Origin` headers.
- Rewrites M3U8 playlists so all segments and keys go through the proxy.
- Strips PNG wrappers from segments (tiktokcdn).
- Works with `hls.js`, VLC, MPV, and any HLS‑compatible player.
## Quick Start
```bash
git clone https://github.com/vyla-entertainment/live-api
npm install
npm start
```
Then open `http://localhost:5000` (or your deployed URL).
## Documentation
- [API Reference](./API.md)
- [Architecture Overview](./ARCHITECTURE.md)
- [Setup & Configuration](./SETUP.md)
- [Deployment (incl. Hugging Face Spaces)](./DEPLOYMENT.md)
- [Debugging](./DEBUGGING.md)
## License
For study and research only. See the original [streamed-pk-hls-stream-resolver](https://github.com/sharoon7171/streamed-pk-hls-stream-resolver) disclaimer.

24
DOCUMENTATION/SETUP.md Normal file
View File

@@ -0,0 +1,24 @@
# Setup & Configuration
## Prerequisites
- **Node.js** ≥ 22 (ES modules support).
- **curl** installed and available in `PATH` (required for proxying).
## Installation
1. Clone the repository:
```bash
git clone https://github.com/vyla-entertainment/live-api
cd live-api
```
2. Install dependencies:
```bash
npm install
```
3. Start the server:
```bash
npm start
```

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
# [./DOCUMENTATION/](./DOCUMENTATION/)
This project is developed and maintained by [Vyla Entertainment](https://github.com/vyla-entertainment) (@vyla-entertainment).
Original work and core authorship: [@sharoon7171](https://github.com/sharoon7171)
Full credits and acknowledgements:
https://github.com/sharoon7171/streamed-pk-hls-stream-resolver

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "streamed-pk-hls-stream-resolver",
"description": "Node.js resolver for streamed.pk and embed.st HLS streams: GOAT protobuf /fetch + lock.wasm decrypt, golf third-party embed chain, curl relay for CDN playlists and MPEG-TS segments.",
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"start": "node server.js"
},
"dependencies": {
"big-integer": "^1.6.52",
"express": "^4.21.2",
"happy-dom": "^20.0.10"
}
}

6
public/assets/title.txt Normal file
View File

@@ -0,0 +1,6 @@
██████╗ ██╗ ██╗██╗ ██╗██╗ █████╗ ███████╗███╗ ██╗████████╗███████╗██████╗ ████████╗ █████╗ ██╗███╗ ██╗███╗ ███╗███████╗███╗ ██╗████████╗
██╔═══██╗██║ ██║╚██╗ ██╔╝██║ ██╔══██╗ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔══██╗╚══██╔══╝██╔══██╗██║████╗ ██║████╗ ████║██╔════╝████╗ ██║╚══██╔══╝
██║██╗██║██║ ██║ ╚████╔╝ ██║ ███████║ █████╗ ██╔██╗ ██║ ██║ █████╗ ██████╔╝ ██║ ███████║██║██╔██╗ ██║██╔████╔██║█████╗ ██╔██╗ ██║ ██║
██║██║██║╚██╗ ██╔╝ ╚██╔╝ ██║ ██╔══██║ ██╔══╝ ██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗ ██║ ██╔══██║██║██║╚██╗██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║ ██║
╚█║████╔╝ ╚████╔╝ ██║ ███████╗██║ ██║ ███████╗██║ ╚████║ ██║ ███████╗██║ ██║ ██║ ██║ ██║██║██║ ╚████║██║ ╚═╝ ██║███████╗██║ ╚████║ ██║
╚╝╚═══╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝

View File

@@ -0,0 +1 @@
v1.0.0

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

7
server.js Normal file
View File

@@ -0,0 +1,7 @@
import app from './src/app.js'
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`Running on http://localhost:${PORT}`)
})

257
src/app.js Normal file
View File

@@ -0,0 +1,257 @@
import express from 'express'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { serve as serveProxy } from './relay/m3u8.js'
import { run } from './resolve/run.js'
import { fetchJson } from './streamed/api.js'
import { fetchLinks } from './streamed/match.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const LOGO_TEXT = fs.readFileSync(path.join(__dirname, '../public/assets/title.txt'), 'utf8')
function getBaseUrl(req) {
return `${req.protocol}://${req.get('host')}`
}
async function upstreamGet(pathname) {
return fetchJson(pathname)
}
async function findMatchById(matchId) {
const sports = await upstreamGet('/api/sports')
if (!Array.isArray(sports)) return null
for (const sport of sports) {
if (!sport?.id) continue
for (const endpoint of [`/api/matches/${encodeURIComponent(sport.id)}`, `/api/matches/${encodeURIComponent(sport.id)}/popular`]) {
const list = await upstreamGet(endpoint)
if (!Array.isArray(list)) continue
const match = list.find((item) => item.id === matchId)
if (match) return match
}
}
return null
}
async function fetchStreamsForMatch(source, id, match) {
const candidates = []
if (source) candidates.push(source)
for (const item of match?.sources || []) {
if (item?.source && !candidates.includes(item.source)) candidates.push(item.source)
}
let lastError = null
for (const candidate of candidates) {
try {
const links = await fetchLinks(candidate, id)
if (Array.isArray(links) && links.length > 0) return { links, sourceUsed: candidate }
} catch (err) {
lastError = err
}
}
throw lastError || new Error('no streams returned for source')
}
export function buildStreamEntry(link, resolved, sourceUsed, id) {
const embedUrl = link.embedUrl || link.watchUrl || link.url || link.href || link.link || null
return {
id,
streamNo: Number(link.streamNo),
language: link.language || link.name || link.title || 'Unknown',
hd: Boolean(link.hd ?? /hd|720|1080/i.test(String(link.language || ''))),
embedUrl,
streamUrl: resolved?.ok ? resolved.relay : null,
source: link.source || sourceUsed,
viewers: Number(link.viewers ?? 0),
}
}
function shapeCard(item) {
return {
id: item.id,
title: item.title,
category: item.category,
startAt: item.startAt,
sources: item.sources || [],
}
}
app.get('/', (req, res) => {
res.type('text/plain; charset=utf-8').send(`${LOGO_TEXT}\n\ndeveloped_by: @vyla-entertainment\ngithub: https://github.com/vyla-entertainment\ndocs: https://docs.vyla.cc\ndmca: https://docs.vyla.cc/misc/dmca`)
})
app.get('/api', (req, res) => {
res.json({
name: 'live-api',
description: 'Live sports aggregator',
endpoints: {
sports: '/api/sports',
matches_by_category: '/api/matches/:category',
popular_matches: '/api/matches/:category/popular',
stream_resolver: '/api/stream/:source/:id',
proxy_m3u8: '/api/m3u8-proxy?url={url}',
images: [
'/api/images/badge/:badge',
'/api/images/poster/:badge1/:badge2',
'/api/images/proxy/:url',
],
},
})
})
app.get('/api/sports', async (req, res) => {
try {
const data = await upstreamGet('/api/sports')
res.json(data)
} catch (e) {
res.status(e.status || 502).json({ error: e.message })
}
})
app.get('/api/matches/:category', async (req, res) => {
try {
const data = await upstreamGet(`/api/matches/${req.params.category}`)
res.json((Array.isArray(data) ? data : []).map(shapeCard))
} catch (e) {
res.status(e.status || 502).json({ error: e.message })
}
})
app.get('/api/matches/:category/popular', async (req, res) => {
try {
const data = await upstreamGet(`/api/matches/${req.params.category}/popular`)
res.json((Array.isArray(data) ? data : []).map(shapeCard))
} catch (e) {
res.status(e.status || 502).json({ error: e.message })
}
})
app.get('/api/stream/:source/:id', async (req, res) => {
try {
const match = await findMatchById(req.params.id)
const { links, sourceUsed } = await fetchStreamsForMatch(req.params.source, req.params.id, match)
const base = getBaseUrl(req)
const enriched = await Promise.all(
links.map(async (link) => {
const inputUrl = link.embedUrl || link.watchUrl || link.url || link.href || link.link
const resolved = inputUrl ? await run({ url: inputUrl }, base) : null
return buildStreamEntry(link, resolved, sourceUsed, req.params.id)
}),
)
res.json(enriched)
} catch (e) {
res.status(502).json({ error: e.message })
}
})
app.post('/api/stream', async (req, res) => {
try {
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const body = JSON.parse(Buffer.concat(chunks).toString())
const data = await run(body, getBaseUrl(req))
res.json(data)
} catch (e) {
res.status(400).json({ ok: false, error: e.message })
}
})
app.get('/api/m3u8-proxy', async (req, res) => {
const target = typeof req.query.url === 'string' ? req.query.url : ''
if (!target) {
res.status(400).end()
return
}
const params = new URLSearchParams()
params.set('url', target)
if (typeof req.query.referer === 'string' && req.query.referer) params.set('referer', req.query.referer)
await serveProxy(res, params, getBaseUrl(req))
})
app.get('/api/hls', async (req, res) => {
const target = typeof req.query.url === 'string' ? req.query.url : ''
if (!target) {
res.status(400).end()
return
}
const params = new URLSearchParams()
params.set('url', target)
if (typeof req.query.referer === 'string' && req.query.referer) params.set('referer', req.query.referer)
if (typeof req.query.embed === 'string' && req.query.embed) params.set('embed', req.query.embed)
if (typeof req.query.embedOrigin === 'string' && req.query.embedOrigin) params.set('embedOrigin', req.query.embedOrigin)
await serveProxy(res, params, getBaseUrl(req))
})
app.get('/api/segment-proxy', async (req, res) => {
const target = typeof req.query.url === 'string' ? req.query.url : ''
if (!target) {
res.status(400).end()
return
}
const params = new URLSearchParams()
params.set('url', target)
if (typeof req.query.referer === 'string' && req.query.referer) params.set('referer', req.query.referer)
await serveProxy(res, params, getBaseUrl(req))
})
app.get('/api/images/badge/:badge', async (req, res) => {
try {
const upstream = await fetch(`https://streamed.pk/api/images/badge/${encodeURIComponent(req.params.badge)}`)
if (!upstream.ok) throw new Error(`image upstream ${upstream.status}`)
res.setHeader('content-type', upstream.headers.get('content-type') || 'application/octet-stream')
for await (const chunk of upstream.body) res.write(chunk)
res.end()
} catch (e) {
res.status(502).json({ error: e.message })
}
})
app.get('/api/images/poster/:badge1/:badge2', async (req, res) => {
try {
const upstream = await fetch(`https://streamed.pk/api/images/poster/${encodeURIComponent(req.params.badge1)}/${encodeURIComponent(req.params.badge2)}`)
if (!upstream.ok) throw new Error(`image upstream ${upstream.status}`)
res.setHeader('content-type', upstream.headers.get('content-type') || 'application/octet-stream')
for await (const chunk of upstream.body) res.write(chunk)
res.end()
} catch (e) {
res.status(502).json({ error: e.message })
}
})
app.get(/^\/api\/images\/proxy\/(.+)$/, async (req, res) => {
try {
const target = decodeURIComponent(req.params[0])
const upstream = await fetch(target)
if (!upstream.ok) throw new Error(`image upstream ${upstream.status}`)
res.setHeader('content-type', upstream.headers.get('content-type') || 'application/octet-stream')
for await (const chunk of upstream.body) res.write(chunk)
res.end()
} catch (e) {
res.status(502).json({ error: e.message })
}
})
app.use(express.static(path.join(__dirname, '../public')))
app.use((req, res) => {
res.status(404).type('text/plain').send('not found')
})
export default app

9
src/env.js Normal file
View File

@@ -0,0 +1,9 @@
export const port = Number(process.env.PORT) || 3000
export const streamedOrigin = process.env.STREAMED_ORIGIN || 'https://streamed.pk'
export const embedOrigin = process.env.EMBED_ORIGIN || 'https://embed.st'
export const ua =
process.env.USER_AGENT ||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'

91
src/http/router.js Normal file
View File

@@ -0,0 +1,91 @@
import { serve } from '../relay/m3u8.js'
import { run } from '../resolve/run.js'
import { fetchLinks } from '../streamed/match.js'
import { serveStatic } from './static.js'
function json(res, status, body) {
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
res.end(JSON.stringify(body))
}
async function readJson(req) {
const chunks = []
for await (const chunk of req) chunks.push(chunk)
return JSON.parse(Buffer.concat(chunks).toString())
}
async function listStreams(source, id, origin) {
const links = await fetchLinks(source, id)
return Promise.all(
links.map(async (link) => {
const inputUrl = link.embedUrl || link.url || link.watchUrl || link.href || link.link
if (!inputUrl) throw new Error('stream url not available')
const resolved = await run({ url: inputUrl }, origin)
if (!resolved.ok) throw new Error(resolved.error || 'resolve failed')
return {
id,
streamNo: Number(link.streamNo),
language: link.language || link.name || link.title || 'Unknown',
hd: Boolean(link.hd ?? /hd|720|1080/i.test(String(link.language || ''))),
streamUrl: resolved.relay,
source: link.source || source,
viewers: Number(link.viewers ?? 0),
}
}),
)
}
export async function route(req, res) {
if (!req.headers.host) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end('missing host')
return
}
const loc = new URL(req.url ?? '/', `http://${req.headers.host}`)
const { pathname, searchParams, origin } = loc
try {
if (pathname === '/api/hls' || pathname === '/api/m3u8-proxy' || pathname === '/api/segment-proxy') {
await serve(res, searchParams, origin)
return
}
const streamMatch = pathname.match(/^\/api\/stream\/([^/]+)\/([^/]+)\/?$/)
if (streamMatch) {
if (req.method !== 'GET') {
json(res, 405, { error: 'GET required' })
return
}
const [, source, id] = streamMatch
json(res, 200, await listStreams(source, id, origin))
return
}
if (pathname === '/api/stream') {
if (req.method !== 'POST') {
json(res, 405, { error: 'POST required' })
return
}
let body
try {
body = await readJson(req)
} catch {
json(res, 400, { ok: false, error: 'invalid json' })
return
}
json(res, 200, await run(body, origin))
return
}
if (serveStatic(pathname, res)) return
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('not found')
} catch (err) {
json(res, 500, { ok: false, error: String(err.message || err) })
}
}

20
src/http/static.js Normal file
View File

@@ -0,0 +1,20 @@
import { createReadStream } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '../../public')
const assets = {
'/': ['index.html', 'text/html; charset=utf-8'],
'/index.html': ['index.html', 'text/html; charset=utf-8'],
'/style.css': ['style.css', 'text/css; charset=utf-8'],
'/player.js': ['player.js', 'application/javascript; charset=utf-8'],
}
export function serveStatic(pathname, res) {
const asset = assets[pathname]
if (!asset) return false
res.writeHead(200, { 'Content-Type': asset[1], 'Cache-Control': 'no-store' })
createReadStream(join(root, asset[0])).pipe(res)
return true
}

17
src/relay/link.js Normal file
View File

@@ -0,0 +1,17 @@
export function relayLink(base, target, slot) {
const q = new URLSearchParams({ url: target, embed: slot.path, embedOrigin: slot.origin })
if (slot.referer) q.set('referer', slot.referer)
return `${base}/api/hls?${q}`
}
export function parseRelaySlot(params) {
const path = params.get('embed')
const origin = params.get('embedOrigin')
if (!path || !origin) throw new Error('embed and embedOrigin required')
const parts = path.split('/')
if (parts.length !== 3 || parts.some((part) => !part)) throw new Error('invalid embed path')
const slot = { origin, path }
const referer = params.get('referer')
if (referer) slot.referer = referer
return slot
}

76
src/relay/m3u8.js Normal file
View File

@@ -0,0 +1,76 @@
import { pull } from '../wire/curl.js'
import { parseRelaySlot, relayLink } from './link.js'
import { segmentBody } from './segment.js'
const cors = { 'Access-Control-Allow-Origin': '*' }
function absUri(uri, base) {
return uri.startsWith('http') ? uri : new URL(uri, base).href
}
function isPlaylist(body) {
const head = body.toString('utf8', 0, Math.min(body.length, 256))
return head.includes('#EXTM3U')
}
function rewrite(text, base, slot, origin) {
return text
.split('\n')
.map((line) => {
const t = line.trim()
if (!t) return line
if (t.startsWith('#')) {
if (!t.includes('URI="')) return line
return t.replace(/URI="([^"]+)"/g, (_, uri) => {
const href = absUri(uri, base)
return `URI="${relayLink(origin, href, slot)}"`
})
}
return relayLink(origin, absUri(t, base), slot)
})
.join('\n')
}
async function relay(res, url, slot, origin) {
const raw = await pull(url, slot)
if (isPlaylist(raw)) {
res.writeHead(200, {
...cors,
'Content-Type': 'application/vnd.apple.mpegurl',
'Cache-Control': 'no-cache',
})
res.end(rewrite(raw.toString('utf8'), url, slot, origin))
return
}
res.writeHead(200, { ...cors, 'Content-Type': 'video/mp2t', 'Cache-Control': 'no-cache' })
res.end(segmentBody(raw))
}
export async function serve(res, params, origin) {
const target = params.get('url')
if (!target) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end('url required')
return
}
let slot
try {
slot = parseRelaySlot(params)
} catch (err) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end(String(err.message || err))
return
}
try {
await relay(res, target, slot, origin)
} catch (err) {
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' })
res.end(String(err.message || err))
}
}
}

23
src/relay/segment.js Normal file
View File

@@ -0,0 +1,23 @@
function tsOff(buf) {
for (let i = 0; i < Math.min(buf.length, 65536); i++) {
if (buf[i] === 0x47 && i + 188 < buf.length && buf[i + 188] === 0x47) return i
}
return -1
}
function strip(buf) {
if (buf.length < 4 || buf[0] === 0x47) return buf
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
const iend = buf.indexOf(Buffer.from('IEND'))
if (iend >= 0 && iend + 8 < buf.length) return buf.subarray(iend + 8)
}
const at = tsOff(buf)
if (at >= 0) return buf.subarray(at)
return buf
}
export function segmentBody(body) {
const out = strip(body)
if (out.length >= 188 && out[0] === 0x47) return out
throw new Error('invalid segment payload')
}

46
src/resolve/parse.js Normal file
View File

@@ -0,0 +1,46 @@
import { embedOrigin } from '../env.js'
import { fetchLinks, pickLink } from '../streamed/match.js'
import { loadWatch, parseWatchPath } from '../streamed/watch.js'
import { makeSlot } from './slot.js'
const embedRe = /^\/embed\/([^/]+)\/([^/]+)\/(\d+)\/?$/
const streamRe = /^\/api\/stream\/([^/]+)\/([^/]+)\/?$/
function parseUrl(raw) {
const s = String(raw || '').trim()
if (!s) throw new Error('url required')
try {
return new URL(s)
} catch {
throw new Error('invalid url')
}
}
function isStreamed(host) {
return host === 'streamed.pk' || host.endsWith('.streamed.pk')
}
function isEmbed(host) {
return host.includes('embed.')
}
async function fromApi(source, id, streamNo) {
if (streamNo == null || streamNo === '') throw new Error('stream number required')
const link = pickLink(await fetchLinks(source, id), streamNo)
return makeSlot(embedOrigin, link.source, link.id, link.streamNo)
}
export async function parseInput(raw) {
const url = parseUrl(raw)
const watch = parseWatchPath(url.pathname)
if (watch && isStreamed(url.hostname)) return (await loadWatch(watch.matchId, watch.source, watch.stream)).slot
const em = url.pathname.match(embedRe)
if (em && isEmbed(url.hostname)) return makeSlot(url.origin, em[1], em[2], em[3])
const api = url.pathname.match(streamRe)
if (api && isStreamed(url.hostname)) return fromApi(api[1], api[2], url.searchParams.get('stream'))
throw new Error('expected /watch/{id}/{source}/{n} or embed.st /embed/{source}/{id}/{n}')
}

53
src/resolve/run.js Normal file
View File

@@ -0,0 +1,53 @@
import { relayLink } from '../relay/link.js'
import { resolve as resolveGolf, relayReferer } from '../sources/golf/resolve.js'
import { postFetch } from '../sources/goat/fetch.js'
import { unlock } from '../sources/goat/lock.js'
import { encodeBody } from '../sources/goat/proto.js'
import { loadWatch } from '../streamed/watch.js'
import { parseInput } from './parse.js'
export async function run(input, origin) {
let slot
let meta
try {
if (input?.matchId) {
if (!input.source || input.stream == null) {
return { ok: false, stage: 'input', error: 'matchId requires source and stream' }
}
meta = await loadWatch(input.matchId, input.source, String(input.stream))
slot = meta.slot
} else if (input?.url?.trim()) {
slot = await parseInput(input.url.trim())
} else {
return { ok: false, stage: 'input', error: 'url or matchId required' }
}
} catch (err) {
return { ok: false, stage: 'input', error: String(err.message || err) }
}
try {
let m3u8
if (slot.source === 'golf') {
m3u8 = await resolveGolf(slot)
slot.referer = relayReferer
} else {
const { body, goat } = await postFetch(encodeBody(slot), slot)
m3u8 = await unlock(slot, goat, body)
}
return {
ok: true,
slug: slot.slug,
source: slot.source,
stream: slot.stream,
matchId: meta?.matchId,
title: meta?.title,
watchUrl: meta?.watchUrl,
embedUrl: meta?.embedUrl,
m3u8,
relay: relayLink(origin, m3u8, slot),
}
} catch (err) {
return { ok: false, stage: 'resolve', error: String(err.message || err) }
}
}

4
src/resolve/slot.js Normal file
View File

@@ -0,0 +1,4 @@
export function makeSlot(origin, source, id, stream) {
const streamNo = String(stream)
return { origin, path: `${source}/${id}/${streamNo}`, source, id, stream: streamNo, slug: id }
}

14
src/server.js Normal file
View File

@@ -0,0 +1,14 @@
import { createServer } from 'node:http'
import { port } from './env.js'
import { route } from './http/router.js'
const srv = createServer(route)
function boot() {
const addr = srv.address()
const host = typeof addr === 'string' ? addr : `localhost:${addr.port}`
console.log(`http://${host}/`)
}
if (process.env.HOST) srv.listen(port, process.env.HOST, boot)
else srv.listen(port, boot)

22
src/sources/goat/fetch.js Normal file
View File

@@ -0,0 +1,22 @@
import { ua } from '../../env.js'
export async function postFetch(body, slot) {
const referer = `${slot.origin}/embed/${slot.path}`
const res = await fetch(`${slot.origin}/fetch`, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
Origin: slot.origin,
Referer: referer,
'User-Agent': ua,
},
body,
})
if (!res.ok) {
const detail = (await res.text()).trim() || res.statusText
throw new Error(`embed /fetch ${res.status}: ${detail}`)
}
const goat = res.headers.get('goat')
if (!goat) throw new Error('missing goat header')
return { body: Buffer.from(await res.arrayBuffer()), goat }
}

View File

@@ -0,0 +1,188 @@
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parentPort, workerData } from 'node:worker_threads'
import { Window } from 'happy-dom'
import { embedOrigin } from '../../env.js'
const vendorDir = join(dirname(fileURLToPath(import.meta.url)), 'vendor')
const wasmPath = join(vendorDir, 'lock.wasm')
const lockModuleUrl = pathToFileURL(join(vendorDir, 'lock-esm.mjs')).href
const wasmBytes = readFileSync(wasmPath)
function pageUrl(slot) {
return `${embedOrigin}/embed/${slot.path}`
}
function mountDom(slot) {
const window = new Window({ url: pageUrl(slot) })
const doc = window.document
doc.body.innerHTML = '<div id="player"></div>'
const jwCfg = { file: null }
const jwBase = {
getContainer: () => doc.getElementById('player'),
getState: () => 'idle',
load: (cfg) => {
if (cfg?.file) jwCfg.file = cfg.file
},
setConfig: (cfg) => {
if (cfg?.file) jwCfg.file = cfg.file
},
getConfig: () => jwCfg,
setup: () => {},
on: () => {},
play: () => {},
getPlaylistItem: () => jwCfg,
getPlaylist: () => (jwCfg.file ? [{ file: jwCfg.file }] : []),
}
window.__wasm_jw_player = new Proxy(jwBase, {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver)
if (prop === Symbol.toStringTag) return 'Object'
return () => null
},
})
window.jwplayer = () => window.__wasm_jw_player
globalThis.window = window
globalThis.document = doc
globalThis.location = window.location
globalThis.self = window
globalThis.atob = (s) => Buffer.from(s, 'base64').toString('binary')
globalThis.btoa = (s) => Buffer.from(s, 'binary').toString('base64')
globalThis.TextDecoder = TextDecoder
globalThis.TextEncoder = TextEncoder
const NativeRequest = globalThis.Request
const NativeResponse = globalThis.Response
const NativeHeaders = globalThis.Headers
const NativeUrl = globalThis.URL
globalThis.URL = class extends NativeUrl {
constructor(input, base) {
if (input === '/fetch') input = `${embedOrigin}/fetch`
super(input, base ?? `${embedOrigin}/`)
}
}
globalThis.Request = class extends NativeRequest {
constructor(input, init) {
if (input === '/fetch') input = `${embedOrigin}/fetch`
super(input, init)
}
}
window.URL = globalThis.URL
window.Request = globalThis.Request
window.Response = NativeResponse
window.Headers = NativeHeaders
return NativeResponse
}
function mockFetch(NativeResponse, goat, body, onM3u8) {
return async (input) => {
const href = typeof input === 'string' ? input : input?.url ?? String(input)
if (href.includes('lock.wasm')) {
return new NativeResponse(wasmBytes, {
status: 200,
headers: { 'Content-Type': 'application/wasm' },
})
}
if (href.includes('/fetch')) {
return new NativeResponse(body, {
status: 200,
headers: { goat, 'Content-Type': 'application/octet-stream' },
})
}
if (href.includes('.m3u8')) {
onM3u8(href)
return new NativeResponse('#EXTM3U\n#EXT-X-VERSION:3\n', {
status: 200,
headers: { 'Content-Type': 'application/vnd.apple.mpegurl' },
})
}
return new NativeResponse('', { status: 404 })
}
}
function patchImports(imports, NativeResponse, goat, body, onM3u8) {
const bg = imports?.['./locked_bg.js']
if (!bg) return
for (const key of Object.keys(bg)) {
if (!key.includes('instanceof')) continue
const orig = bg[key]
bg[key] = (...args) => (orig(...args) ? 1 : 1)
}
const fetchKey = Object.keys(bg).find((k) => k.includes('fetch_e6e8e0'))
if (!fetchKey) return
bg[fetchKey] = (_win, req) => {
const href = req?.url ?? ''
if (href.includes('/fetch')) {
return Promise.resolve(
new NativeResponse(body, {
status: 200,
headers: { goat, 'Content-Type': 'application/octet-stream' },
}),
)
}
if (href.includes('.m3u8')) {
onM3u8(href)
return Promise.resolve(
new NativeResponse('#EXTM3U\n#EXT-X-VERSION:3\n', {
status: 200,
headers: { 'Content-Type': 'application/vnd.apple.mpegurl' },
}),
)
}
return Promise.reject(new Error(`unexpected wasm fetch ${href}`))
}
}
async function crack(slot, goat, bodyHex) {
let m3u8 = null
const body = Buffer.from(bodyHex, 'hex')
const NativeResponse = mountDom(slot)
const fetchFn = mockFetch(NativeResponse, goat, body, (url) => {
m3u8 = url
})
globalThis.fetch = fetchFn
const origInstantiate = WebAssembly.instantiate.bind(WebAssembly)
WebAssembly.instantiate = async (source, imports) => {
patchImports(imports, NativeResponse, goat, body, (url) => {
m3u8 = url
})
if (!(source instanceof ArrayBuffer) && !ArrayBuffer.isView(source)) {
source = wasmBytes.buffer.slice(wasmBytes.byteOffset, wasmBytes.byteOffset + wasmBytes.byteLength)
}
return origInstantiate(source, imports)
}
WebAssembly.instantiateStreaming = async (_resp, imports) => WebAssembly.instantiate(wasmBytes, imports)
const mod = await import(lockModuleUrl)
const api = await mod.default({
module_or_path: `${embedOrigin}/js/wasm/lock.wasm`,
fetch: fetchFn,
})
await api.init_wasm?.()
WebAssembly.instantiate = origInstantiate
delete WebAssembly.instantiateStreaming
try {
await api.set_stream_jw(slot.source, slot.id, slot.stream)
} catch (err) {
if (!m3u8) throw err
}
if (!m3u8) throw new Error('lock did not yield m3u8')
return m3u8
}
const { slot, goat, bodyHex } = workerData
crack(slot, goat, bodyHex)
.then((url) => parentPort.postMessage({ ok: true, url }))
.catch((err) => parentPort.postMessage({ ok: false, error: String(err.message || err) }))

20
src/sources/goat/lock.js Normal file
View File

@@ -0,0 +1,20 @@
import { Worker } from 'node:worker_threads'
const workerUrl = new URL('./lock-worker.js', import.meta.url)
export function unlock(slot, goat, body) {
return new Promise((resolve, reject) => {
const worker = new Worker(workerUrl, {
workerData: { slot, goat, bodyHex: body.toString('hex') },
})
worker.once('message', (msg) => {
worker.terminate().catch(() => {})
if (msg.ok) resolve(msg.url)
else reject(new Error(msg.error || 'lock decrypt failed'))
})
worker.once('error', (err) => {
worker.terminate().catch(() => {})
reject(err)
})
})
}

25
src/sources/goat/proto.js Normal file
View File

@@ -0,0 +1,25 @@
function varint(n) {
const bytes = []
let v = n
while (v > 0x7f) {
bytes.push((v & 0x7f) | 0x80)
v >>>= 7
}
bytes.push(v)
return Buffer.from(bytes)
}
function fieldStr(out, field, value) {
const body = Buffer.from(String(value), 'utf8')
out.push(Buffer.from([(field << 3) | 2]))
out.push(varint(body.length))
out.push(body)
}
export function encodeBody({ source, id, stream }) {
const out = []
fieldStr(out, 1, source)
fieldStr(out, 2, id)
fieldStr(out, 3, stream)
return Buffer.concat(out)
}

6
src/sources/goat/vendor/lock-esm.mjs vendored Normal file

File diff suppressed because one or more lines are too long

BIN
src/sources/goat/vendor/lock.wasm vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,32 @@
import { fetchHeaders } from '../../wire/headers.js'
export const relayReferer = 'https://exposestrat.com/'
function iframeUrl(html) {
const m = html.match(/iframe src="([^"]+)"/)
if (!m) throw new Error('golf embed iframe not found')
return m[1].replace(/&amp;/g, '&')
}
function fid(html) {
const m = html.match(/fid="([^"]+)"/)
if (!m) throw new Error('golf stream fid not found')
return m[1]
}
function m3u8Url(html) {
const m = html.match(/return\(\[("[^"]+"(?:,"[^"]+")*)\]\.join\(""\)/)
if (!m) throw new Error('golf m3u8 url not found')
return JSON.parse(`[${m[1]}]`).join('')
}
export async function resolve(slot) {
const embedUrl = `${slot.origin}/embed/${slot.path}`
const embedHtml = await (await fetch(embedUrl, { headers: fetchHeaders(`${slot.origin}/`) })).text()
const streamedUrl = iframeUrl(embedHtml)
const streamedHtml = await (await fetch(streamedUrl, { headers: fetchHeaders(embedUrl) })).text()
const live = fid(streamedHtml)
const playerUrl = `https://exposestrat.com/maestrohd1.php?player=desktop&live=${encodeURIComponent(live)}`
const playerHtml = await (await fetch(playerUrl, { headers: fetchHeaders(streamedUrl) })).text()
return m3u8Url(playerHtml)
}

10
src/streamed/api.js Normal file
View File

@@ -0,0 +1,10 @@
import { fetchHeaders } from '../wire/headers.js'
import { streamedOrigin } from '../env.js'
export async function fetchJson(path) {
const res = await fetch(`${streamedOrigin}${path}`, {
headers: fetchHeaders(undefined, { Accept: 'application/json' }),
})
if (!res.ok) throw new Error(`streamed.pk ${path} ${res.status}`)
return res.json()
}

15
src/streamed/match.js Normal file
View File

@@ -0,0 +1,15 @@
import { fetchJson } from './api.js'
export async function fetchLinks(source, id) {
const links = await fetchJson(`/api/stream/${encodeURIComponent(source)}/${encodeURIComponent(id)}`)
if (!Array.isArray(links) || links.length === 0) throw new Error('no streams returned for source')
return links
}
export function pickLink(links, streamNo) {
if (streamNo == null || streamNo === '') throw new Error('stream number required')
const wanted = Number(streamNo)
const picked = links.find((link) => Number(link.streamNo) === wanted)
if (!picked) throw new Error(`stream ${streamNo} not found`)
return picked
}

74
src/streamed/watch.js Normal file
View File

@@ -0,0 +1,74 @@
import { embedOrigin, streamedOrigin } from '../env.js'
import { fetchJson } from './api.js'
import { fetchLinks, pickLink } from './match.js'
import { makeSlot } from '../resolve/slot.js'
const watchRe = /^\/watch\/([^/]+)\/([^/]+)\/(\d+)\/?$/
async function loadMatch(matchId) {
const sports = await fetchJson('/api/sports')
if (!Array.isArray(sports)) throw new Error('streamed.pk /api/sports invalid response')
for (const sport of sports) {
if (!sport?.id) continue
for (const endpoint of [`/api/matches/${encodeURIComponent(sport.id)}`, `/api/matches/${encodeURIComponent(sport.id)}/popular`]) {
const list = await fetchJson(endpoint)
if (!Array.isArray(list)) continue
const match = list.find((item) => item.id === matchId)
if (match) return match
}
}
throw new Error(`match not found: ${matchId}`)
}
async function loadLinks(matchId, source, stream) {
const match = await loadMatch(matchId)
const sources = match.sources ?? []
const candidates = [source, ...sources.map((item) => item.source)].filter(Boolean)
let lastError = null
for (const candidate of candidates) {
try {
const src = candidate === source ? pickSource(match, candidate) : sources.find((item) => item.source === candidate)
if (!src?.source || !src?.id) continue
const link = pickLink(await fetchLinks(src.source, src.id), stream)
return { match, link }
} catch (err) {
lastError = err
}
}
throw lastError || new Error(`source ${source} not found`)
}
function pickSource(match, name) {
const sources = match.sources ?? []
if (!sources.length) throw new Error('match has no stream sources')
const picked = sources.find((item) => item.source === name)
if (!picked) throw new Error(`source ${name} not found`)
return picked
}
function watchLink(matchId, source, stream) {
return `${streamedOrigin}/watch/${matchId}/${source}/${Number(stream)}`
}
export function parseWatchPath(pathname) {
const m = pathname.match(watchRe)
if (!m) return null
return { matchId: m[1], source: m[2], stream: m[3] }
}
export async function loadWatch(matchId, source, stream) {
const { match, link } = await loadLinks(matchId, source, stream)
return {
matchId: match.id,
title: match.title,
watchUrl: watchLink(match.id, link.source, link.streamNo),
embedUrl: link.embedUrl,
slot: makeSlot(embedOrigin, link.source, link.id, link.streamNo),
}
}

34
src/wire/curl.js Normal file
View File

@@ -0,0 +1,34 @@
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { fetchHeaders } from './headers.js'
const exec = promisify(execFile)
function hdrs(slot) {
const referer = slot.referer || `${slot.origin}/embed/${slot.path}`
return {
...fetchHeaders(referer),
Origin: slot.referer ? new URL(referer).origin : slot.origin,
Accept: '*/*',
}
}
function curlArgs(url, headers) {
const args = ['-sS', '-L', '--compressed']
for (const [key, value] of Object.entries(headers)) args.push('-H', `${key}: ${value}`)
args.push(url)
return args
}
export async function pull(url, slot) {
const headers = hdrs(slot)
const args = curlArgs(url, headers)
args.push('-o', '-', '-w', 'HTTPSTATUS:%{http_code}')
const { stdout } = await exec('curl', args, { maxBuffer: 64 * 1024 * 1024, encoding: 'buffer' })
const mark = stdout.lastIndexOf(Buffer.from('HTTPSTATUS:'))
if (mark < 0) throw new Error('curl response parse failed')
const body = stdout.subarray(0, mark)
const code = Number(stdout.subarray(mark + 11).toString('utf8'))
if (code < 200 || code >= 300) throw new Error(`upstream ${code}`)
return body
}

7
src/wire/headers.js Normal file
View File

@@ -0,0 +1,7 @@
import { ua } from '../env.js'
export function fetchHeaders(referer, extra = {}) {
const headers = { 'User-Agent': ua, ...extra }
if (referer) headers.Referer = referer
return headers
}

175
test/api.test.js Normal file
View File

@@ -0,0 +1,175 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createServer } from 'node:http'
import { once } from 'node:events'
import { buildStreamEntry } from '../src/app.js'
function makeJsonResponse(body, init = {}) {
return new Response(JSON.stringify(body), {
status: init.status || 200,
headers: { 'content-type': 'application/json', ...(init.headers || {}) },
})
}
test('root responds with plain text logo', async () => {
const { default: app } = await import('../src/app.js')
const server = createServer(app)
await once(server.listen(0, '127.0.0.1'), 'listening')
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
try {
const response = await fetch(`http://127.0.0.1:${port}/`)
assert.equal(response.status, 200)
assert.match(response.headers.get('content-type') ?? '', /text\/plain/)
const text = await response.text()
assert.match(text, /developed_by: @vyla-entertainment/)
assert.doesNotMatch(text, /<html/i)
} finally {
await new Promise((resolve) => server.close(resolve))
}
})
test('api routes fall back to the match source when the requested source is empty', async () => {
const originalFetch = globalThis.fetch
const calls = []
globalThis.fetch = async (input) => {
const url = String(input)
calls.push(url)
if (url.endsWith('/api/sports')) {
return makeJsonResponse([{ id: 'football', name: 'Football' }])
}
if (url.endsWith('/api/matches/football')) {
return makeJsonResponse([
{
id: 'sporting-cp-vs-strasbourg-game-399075',
title: 'Sporting CP vs Strasbourg',
category: 'football',
startAt: '2026-07-19T00:00:00Z',
sources: [{ source: 'echo', id: 'sporting-cp-vs-strasbourg-game-399075' }],
},
])
}
if (url.endsWith('/api/matches/football/popular')) {
return makeJsonResponse([])
}
if (url.endsWith('/api/stream/admin/sporting-cp-vs-strasbourg-game-399075')) {
return makeJsonResponse([])
}
if (url.endsWith('/api/stream/echo/sporting-cp-vs-strasbourg-game-399075')) {
return makeJsonResponse([
{
id: 'sporting-cp-vs-strasbourg-game-399075',
streamNo: 1,
language: 'English',
hd: true,
embedUrl: 'https://embed.example/1',
source: 'echo',
},
])
}
throw new Error(`unexpected fetch: ${url}`)
}
try {
const { default: app } = await import('../src/app.js')
const server = createServer(app)
await once(server.listen(0, '127.0.0.1'), 'listening')
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
try {
const sports = await originalFetch(`http://127.0.0.1:${port}/api/sports`)
assert.equal(sports.status, 200)
assert.deepEqual(await sports.json(), [{ id: 'football', name: 'Football' }])
const matches = await originalFetch(`http://127.0.0.1:${port}/api/matches/football`)
assert.equal(matches.status, 200)
const matchJson = await matches.json()
assert.equal(matchJson[0].id, 'sporting-cp-vs-strasbourg-game-399075')
assert.deepEqual(matchJson[0].sources, [{ source: 'echo', id: 'sporting-cp-vs-strasbourg-game-399075' }])
const streams = await originalFetch(`http://127.0.0.1:${port}/api/stream/admin/sporting-cp-vs-strasbourg-game-399075`)
assert.equal(streams.status, 200)
const streamJson = await streams.json()
assert.equal(streamJson[0].embedUrl, 'https://embed.example/1')
assert(calls.includes('https://streamed.pk/api/sports'))
assert(calls.includes('https://streamed.pk/api/matches/football'))
assert(calls.includes('https://streamed.pk/api/stream/admin/sporting-cp-vs-strasbourg-game-399075'))
assert(calls.includes('https://streamed.pk/api/stream/echo/sporting-cp-vs-strasbourg-game-399075'))
} finally {
await new Promise((resolve) => server.close(resolve))
}
} finally {
globalThis.fetch = originalFetch
}
})
test('buildStreamEntry preserves the proxied relay URL', () => {
const entry = buildStreamEntry(
{ streamNo: 1, language: 'English', hd: true, embedUrl: 'https://embed.example/1', source: 'delta' },
{ ok: true, relay: 'http://127.0.0.1:5000/api/m3u8-proxy?url=https%3A%2F%2Fupstream.example%2Fplaylist.m3u8' },
'delta',
'live-event_world-cup-champions-celebration-live-stream',
)
assert.equal(entry.embedUrl, 'https://embed.example/1')
assert.equal(entry.streamUrl, 'http://127.0.0.1:5000/api/m3u8-proxy?url=https%3A%2F%2Fupstream.example%2Fplaylist.m3u8')
})
test('api hls route proxies playlists instead of falling through to 404', async () => {
const upstream = createServer((req, res) => {
if (req.url === '/playlist.m3u8') {
assert.equal(req.headers.referer, 'https://embed.st/embed/echo/demo/1')
assert.equal(req.headers.origin, 'https://embed.st')
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl' })
res.end('#EXTM3U\n#EXT-X-VERSION:3\n#EXTINF:10,\nsegment.ts\n')
return
}
if (req.url === '/segment.ts') {
res.writeHead(200, { 'content-type': 'video/mp2t' })
res.end('segment-bytes')
return
}
res.writeHead(404)
res.end('not found')
})
await once(upstream.listen(0, '127.0.0.1'), 'listening')
const upstreamAddress = upstream.address()
const upstreamPort = typeof upstreamAddress === 'object' && upstreamAddress ? upstreamAddress.port : 0
try {
const { default: app } = await import('../src/app.js')
const server = createServer(app)
await once(server.listen(0, '127.0.0.1'), 'listening')
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
try {
const response = await fetch(
`http://127.0.0.1:${port}/api/hls?url=${encodeURIComponent(`http://127.0.0.1:${upstreamPort}/playlist.m3u8`)}&embed=echo%2Fdemo%2F1&embedOrigin=${encodeURIComponent('https://embed.st')}`,
)
assert.equal(response.status, 200)
assert.match(response.headers.get('content-type') ?? '', /mpegurl/i)
const text = await response.text()
assert.match(text, /#EXTM3U/)
assert.match(text, /\/api\/hls\?url=/)
} finally {
await new Promise((resolve) => server.close(resolve))
}
} finally {
await new Promise((resolve) => upstream.close(resolve))
}
})