api & packages: rewrite hls parser with native rust module

This commit is contained in:
Abdulhaq 2026-07-02 03:26:01 +03:00
parent a636575b09
commit a408f69a38
12 changed files with 1910 additions and 58 deletions

4
.gitignore vendored
View file

@ -20,3 +20,7 @@ docker-compose.yml
# ide
.vscode
# rust
target/
*.node

View file

@ -26,6 +26,7 @@
"@datastructures-js/priority-queue": "^6.3.5",
"@imput/psl": "^2.0.4",
"@imput/version-info": "workspace:^",
"@imput/cobalt-rs-hls": "workspace:*",
"content-disposition-header": "0.6.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",

View file

@ -1,6 +1,6 @@
import HLS from "hls-parser";
import { createInternalStream } from "./manage.js";
import { request } from "undici";
import { HlsParser } from "@imput/cobalt-rs-hls";
function getURL(url) {
try {
@ -10,50 +10,6 @@ function getURL(url) {
}
}
function transformObject(streamInfo, hlsObject) {
if (hlsObject === undefined) {
return (object) => transformObject(streamInfo, object);
}
let fullUrl;
if (getURL(hlsObject.uri)) {
fullUrl = new URL(hlsObject.uri);
} else {
fullUrl = new URL(hlsObject.uri, streamInfo.url);
}
if (fullUrl.hostname !== '127.0.0.1') {
hlsObject.uri = createInternalStream(fullUrl.toString(), streamInfo);
if (hlsObject.map) {
hlsObject.map = transformObject(streamInfo, hlsObject.map);
}
}
return hlsObject;
}
function transformMasterPlaylist(streamInfo, hlsPlaylist) {
const makeInternalStream = transformObject(streamInfo);
const makeInternalVariants = (variant) => {
variant = transformObject(streamInfo, variant);
variant.video = variant.video.map(makeInternalStream);
variant.audio = variant.audio.map(makeInternalStream);
return variant;
};
hlsPlaylist.variants = hlsPlaylist.variants.map(makeInternalVariants);
return hlsPlaylist;
}
function transformMediaPlaylist(streamInfo, hlsPlaylist) {
const makeInternalSegments = transformObject(streamInfo);
hlsPlaylist.segments = hlsPlaylist.segments.map(makeInternalSegments);
hlsPlaylist.prefetchSegments = hlsPlaylist.prefetchSegments.map(makeInternalSegments);
return hlsPlaylist;
}
const HLS_MIME_TYPES = ["application/vnd.apple.mpegurl", "audio/mpegurl", "application/x-mpegURL"];
export function isHlsResponse(req, streamInfo) {
@ -64,16 +20,14 @@ export function isHlsResponse(req, streamInfo) {
}
export async function handleHlsPlaylist(streamInfo, req, res) {
let hlsPlaylist = await req.body.text();
hlsPlaylist = HLS.parse(hlsPlaylist);
const playlistContent = await req.body.text();
hlsPlaylist = hlsPlaylist.isMasterPlaylist
? transformMasterPlaylist(streamInfo, hlsPlaylist)
: transformMediaPlaylist(streamInfo, hlsPlaylist);
const parser = new HlsParser(streamInfo.url);
const result = parser.handleHlsPlaylist(playlistContent, (uri) => {
return createInternalStream(uri, streamInfo);
});
hlsPlaylist = HLS.stringify(hlsPlaylist);
res.send(hlsPlaylist);
res.send(result);
}
async function getSegmentSize(url, config) {
@ -109,15 +63,23 @@ export async function probeInternalHLSTunnel(streamInfo) {
const config = { headers, dispatcher, signal, maxRedirections: 16 };
const manifestResponse = await fetch(url, config);
const manifestText = await manifestResponse.text();
const manifest = HLS.parse(await manifestResponse.text());
if (manifest.segments.length === 0)
const parser = new HlsParser(streamInfo.url);
let segments;
try {
segments = parser.getMediaSegments(manifestText);
} catch (e) {
return -1;
}
if (!segments || segments.length === 0)
return -1;
const segmentSamples = await Promise.all(
Array(5).fill().map(async () => {
const manifestIdx = Math.floor(Math.random() * manifest.segments.length);
const randomSegment = manifest.segments[manifestIdx];
const manifestIdx = Math.floor(Math.random() * segments.length);
const randomSegment = segments[manifestIdx];
if (!randomSegment.uri)
throw "segment is missing URI";
@ -135,7 +97,7 @@ export async function probeInternalHLSTunnel(streamInfo) {
);
const averageBitrate = segmentSamples.reduce((a, b) => a + b) / segmentSamples.length;
const totalDuration = manifest.segments.reduce((acc, segment) => acc + segment.duration, 0);
const totalDuration = segments.reduce((acc, segment) => acc + segment.duration, 0);
return averageBitrate * totalDuration;
}

View file

@ -39,6 +39,7 @@ requirements:
- node.js >= 18
- git
- pnpm
- rust (cargo) for the high-performance HLS parsing module
1. clone the repo: `git clone https://github.com/imputnet/cobalt`.
2. go to api directory: `cd cobalt/api`.

572
packages/cobalt-rs-hls/Cargo.lock generated Normal file
View file

@ -0,0 +1,572 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"num-traits",
]
[[package]]
name = "cobalt-rs-hls"
version = "0.0.0"
dependencies = [
"m3u8-rs",
"napi",
"napi-build",
"napi-derive",
"url",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "displaydoc"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_properties"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
name = "icu_provider"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "m3u8-rs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03cd3335fb5f2447755d45cda9c70f76013626a9db44374973791b0926a86c3"
dependencies = [
"chrono",
"nom",
]
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "napi"
version = "2.16.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
dependencies = [
"bitflags",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
]
[[package]]
name = "napi-build"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
[[package]]
name = "napi-derive"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [
"libloading",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -0,0 +1,16 @@
[package]
edition = "2021"
name = "cobalt-rs-hls"
version = "0.0.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = { version = "2.16.2", default-features = false, features = ["napi4"] }
napi-derive = "2.16.2"
m3u8-rs = "6.0.0"
url = "2.5.0"
[build-dependencies]
napi-build = "2.1.3"

View file

@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

0
packages/cobalt-rs-hls/index.d.ts vendored Normal file
View file

View file

@ -0,0 +1,7 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// we load the compiled native binary directly for now
const { HlsParser } = require('./cobalt-rs-hls.darwin-arm64.node');
export { HlsParser };

View file

@ -0,0 +1,21 @@
{
"name": "@imput/cobalt-rs-hls",
"version": "11.7.0",
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"napi": {
"name": "cobalt-rs-hls",
"triples": {
"defaults": true,
"additional": []
}
},
"scripts": {
"build": "napi build --platform --release",
"build:debug": "napi build --platform"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0"
}
}

View file

@ -0,0 +1,125 @@
#![deny(clippy::all)]
#[macro_use]
extern crate napi_derive;
use m3u8_rs::{MasterPlaylist, MediaPlaylist, Playlist};
use napi::bindgen_prelude::*;
use napi::JsString;
use url::Url;
#[napi(object)]
pub struct MediaSegment {
pub uri: String,
pub duration: f64,
}
#[napi]
pub struct HlsParser {
stream_info_url: String,
}
#[napi]
impl HlsParser {
#[napi(constructor)]
pub fn new(stream_info_url: String) -> Self {
Self { stream_info_url }
}
#[napi]
pub fn handle_hls_playlist(
&self,
env: Env,
playlist_content: String,
create_internal_stream: JsFunction,
) -> Result<String> {
// parse the raw hls text
match m3u8_rs::parse_playlist_res(playlist_content.as_bytes()) {
Ok(Playlist::MasterPlaylist(mut master)) => {
self.transform_master_playlist(&env, &mut master, &create_internal_stream)?;
let mut buf = Vec::new();
master.write_to(&mut buf).map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
Ok(String::from_utf8(buf).unwrap())
}
Ok(Playlist::MediaPlaylist(mut media)) => {
self.transform_media_playlist(&env, &mut media, &create_internal_stream)?;
let mut buf = Vec::new();
media.write_to(&mut buf).map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
Ok(String::from_utf8(buf).unwrap())
}
Err(e) => {
Err(Error::new(Status::InvalidArg, format!("failed to parse hls: {:?}", e)))
}
}
}
#[napi]
pub fn get_media_segments(&self, playlist_content: String) -> Result<Vec<MediaSegment>> {
match m3u8_rs::parse_playlist_res(playlist_content.as_bytes()) {
Ok(Playlist::MediaPlaylist(media)) => {
let segments = media.segments.into_iter().map(|s| MediaSegment {
uri: s.uri,
duration: s.duration as f64,
}).collect();
Ok(segments)
}
Ok(Playlist::MasterPlaylist(_)) => {
Err(Error::new(Status::InvalidArg, "expected media playlist, got master".to_string()))
}
Err(e) => {
Err(Error::new(Status::InvalidArg, format!("failed to parse hls: {:?}", e)))
}
}
}
fn resolve_url(&self, uri: &str) -> Option<Url> {
if let Ok(full_url) = Url::parse(uri) {
Some(full_url)
} else if let Ok(base_url) = Url::parse(&self.stream_info_url) {
base_url.join(uri).ok()
} else {
None
}
}
fn transform_uri(
&self,
env: &Env,
uri: &mut String,
create_internal_stream: &JsFunction,
) -> Result<()> {
if let Some(full_url) = self.resolve_url(uri) {
if full_url.host_str() != Some("127.0.0.1") {
// call back into node to create the internal stream
let js_string = env.create_string(&full_url.to_string())?;
let result: JsString = create_internal_stream.call(None, &[js_string])?.try_into()?;
*uri = result.into_utf8()?.into_owned()?;
}
}
Ok(())
}
fn transform_master_playlist(
&self,
env: &Env,
master: &mut MasterPlaylist,
create_internal_stream: &JsFunction,
) -> Result<()> {
for variant in &mut master.variants {
self.transform_uri(env, &mut variant.uri, create_internal_stream)?;
}
Ok(())
}
fn transform_media_playlist(
&self,
env: &Env,
media: &mut MediaPlaylist,
create_internal_stream: &JsFunction,
) -> Result<()> {
for segment in &mut media.segments {
self.transform_uri(env, &mut segment.uri, create_internal_stream)?;
}
Ok(())
}
}

1138
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff