{"_id":"@aliran/player-sdk","_rev":"4-2fc1afe90af05d820d07c238d2db6790","name":"@aliran/player-sdk","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"@aliran/player-sdk","version":"0.1.0","keywords":["p2p","streaming","ott","hls","player","holepunch","hyperswarm","hyperdrive","oprf","aliran"],"license":"MIT","_id":"@aliran/player-sdk@0.1.0","maintainers":[{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"}],"homepage":"https://abuelosimpson.github.io/aliran/sdk/","bugs":{"url":"https://github.com/AbueloSimpson/aliran/issues"},"dist":{"shasum":"b795d71d3f60be1de77aeb0fb40c27722ac211af","tarball":"https://registry.npmjs.org/@aliran/player-sdk/-/player-sdk-0.1.0.tgz","fileCount":9,"integrity":"sha512-2Ub/thBgWtwf3Uidnl3T9fgP2onlljXOQB+8aEcFkYg45iHBPtdEzyJXPrhNPlcTbIOt4vWT4lFLBvBAyQXJeA==","signatures":[{"sig":"MEUCIQD2jA8nLoasccKb75xXGhfrozX+4waFV4TmlueafLrXnAIgbMUuPUmc84+m1bejX3KnG60dxaRqj471XYdN2L+LnSg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":123443},"main":"index.js","type":"module","types":"index.d.ts","comment":"Runtime-agnostic: player.js takes injected { http, fs } so it runs in Node (index.js wires node builtins) and in the Bare worklet (client/backend/backend.mjs wires bare-http1/bare-fs). Keep dependency ranges in sync with client/backend/package.json — the Android bundle resolves this same graph. @aliran/core is a semver range for publishability; inside the repo the npm workspace (root package.json) and client/backend's direct file:../../core dep both satisfy it with the local copy — nothing is fetched from the registry.","engines":{"node":">=20"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./login.js":"./login.js","./serve.js":"./serve.js","./player.js":"./player.js","./recover.js":"./recover.js","./package.json":"./package.json"},"gitHead":"140e299e79f56c6f011a14f56cdc4cd0ccfc5d04","scripts":{"test":"node test.mjs"},"_npmUser":{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"},"repository":{"url":"git+https://github.com/AbueloSimpson/aliran.git","type":"git","directory":"sdk"},"_npmVersion":"11.13.0","description":"Headless Aliran player engine — P2P OPRF login, catalog, and localhost HLS serving (Node + Bare)","directories":{},"_nodeVersion":"24.17.0","dependencies":{"b4a":"^1.6.6","rache":"^1.0.0","hyperbee":"^2.20.0","corestore":"^6.18.0","hyperdrive":"^11.13.0","hyperswarm":"^4.8.0","@aliran/core":"^0.1.0","protomux-rpc":"^1.5.0","hypercore-crypto":"^3.4.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/player-sdk_0.1.0_1784688068710_0.7678303744504571","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@aliran/player-sdk","version":"0.1.1","keywords":["p2p","streaming","ott","hls","player","holepunch","hyperswarm","hyperdrive","oprf","aliran"],"license":"MIT","_id":"@aliran/player-sdk@0.1.1","maintainers":[{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"}],"homepage":"https://abuelosimpson.github.io/aliran/sdk/","bugs":{"url":"https://github.com/AbueloSimpson/aliran/issues"},"dist":{"shasum":"12dc04f5cbdf7e5b6c0e1cd72fb0e23dfd0117ab","tarball":"https://registry.npmjs.org/@aliran/player-sdk/-/player-sdk-0.1.1.tgz","fileCount":9,"integrity":"sha512-eEb2mx+DeWLhgBX8UrpFbzuOAH/0UObcLbjoihz//XiC/Qehu0Hba/R6qssQPcWjePNhN6ZbVLMcFCyOSFO03w==","signatures":[{"sig":"MEUCIGjx6yj71jHC25ImKIjihoFOewPOW+kFY/1LyGFhh4QrAiEAiTVYNPLIyaB3kTW/Dk2MkJq60vTytR0aadhAo+5cefQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":128250},"main":"index.js","type":"module","types":"index.d.ts","comment":"Runtime-agnostic: player.js takes injected { http, fs } so it runs in Node (index.js wires node builtins) and in the Bare worklet (client/backend/backend.mjs wires bare-http1/bare-fs). Keep dependency ranges in sync with client/backend/package.json — the Android bundle resolves this same graph. @aliran/core is a semver range for publishability; inside the repo the npm workspace (root package.json) and client/backend's direct file:../../core dep both satisfy it with the local copy — nothing is fetched from the registry.","engines":{"node":">=20"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./login.js":"./login.js","./serve.js":"./serve.js","./player.js":"./player.js","./recover.js":"./recover.js","./package.json":"./package.json"},"gitHead":"3c15b9b3929a044e97082e43e62b148c7c05e3e9","scripts":{"test":"node test.mjs"},"_npmUser":{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"},"repository":{"url":"git+https://github.com/AbueloSimpson/aliran.git","type":"git","directory":"sdk"},"_npmVersion":"11.13.0","description":"Headless Aliran player engine — P2P OPRF login, catalog, and localhost HLS serving (Node + Bare)","directories":{},"_nodeVersion":"24.17.0","dependencies":{"b4a":"^1.6.6","rache":"^1.0.0","hyperbee":"^2.20.0","corestore":"^6.18.0","hyperdrive":"^11.13.0","hyperswarm":"^4.8.0","@aliran/core":"^0.1.1","protomux-rpc":"^1.5.0","hypercore-crypto":"^3.4.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/player-sdk_0.1.1_1784695414934_0.19166756337024982","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@aliran/player-sdk","version":"0.1.2","keywords":["p2p","streaming","ott","hls","player","holepunch","hyperswarm","hyperdrive","oprf","aliran"],"license":"MIT","_id":"@aliran/player-sdk@0.1.2","maintainers":[{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"}],"homepage":"https://abuelosimpson.github.io/aliran/sdk/","bugs":{"url":"https://github.com/AbueloSimpson/aliran/issues"},"dist":{"shasum":"cf317052ba7b0313ccb96ea7785261d75b8742b0","tarball":"https://registry.npmjs.org/@aliran/player-sdk/-/player-sdk-0.1.2.tgz","fileCount":9,"integrity":"sha512-cG4KwCywa7ij4nbmYRx2HzGsUKRU5x3bZzqJCHrO8UO3dyyxh7wdHLu638vKifuy+h1iX3D93Lpx/ORGqiynJw==","signatures":[{"sig":"MEYCIQDxPG/o/yZgWy6uZHIUUQNNgYGW1xa8C6pZK6jCbwAX1gIhAPu5MPD8HU+y/DzuHhXc6a4t5nLOme6EKt8hOdI3UMBB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":135356},"main":"index.js","type":"module","types":"index.d.ts","comment":"Runtime-agnostic: player.js takes injected { http, fs } so it runs in Node (index.js wires node builtins) and in the Bare worklet (client/backend/backend.mjs wires bare-http1/bare-fs). Keep dependency ranges in sync with client/backend/package.json — the Android bundle resolves this same graph. @aliran/core is a semver range for publishability; inside the repo the npm workspace (root package.json) and client/backend's direct file:../../core dep both satisfy it with the local copy — nothing is fetched from the registry.","engines":{"node":">=20"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./login.js":"./login.js","./serve.js":"./serve.js","./player.js":"./player.js","./recover.js":"./recover.js","./package.json":"./package.json"},"gitHead":"64ca86553f7bfe677867c78db1f12df00d7ee3d5","scripts":{"test":"node test.mjs"},"_npmUser":{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"},"repository":{"url":"git+https://github.com/AbueloSimpson/aliran.git","type":"git","directory":"sdk"},"_npmVersion":"11.13.0","description":"Headless Aliran player engine — P2P OPRF login, catalog, and localhost HLS serving (Node + Bare)","directories":{},"_nodeVersion":"24.17.0","dependencies":{"b4a":"^1.6.6","rache":"^1.0.0","hyperbee":"^2.20.0","corestore":"^6.18.0","hyperdrive":"^11.13.0","hyperswarm":"^4.8.0","@aliran/core":"^0.1.1","protomux-rpc":"^1.5.0","hypercore-crypto":"^3.4.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/player-sdk_0.1.2_1784778760482_0.17038917934843534","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aliran/player-sdk","version":"0.1.3","description":"Headless Aliran player engine — P2P OPRF login, catalog, and localhost HLS serving (Node + Bare)","license":"MIT","type":"module","main":"index.js","types":"index.d.ts","exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./player.js":"./player.js","./login.js":"./login.js","./pairing.js":"./pairing.js","./report.js":"./report.js","./recover.js":"./recover.js","./serve.js":"./serve.js","./package.json":"./package.json"},"repository":{"type":"git","url":"git+https://github.com/AbueloSimpson/aliran.git","directory":"sdk"},"homepage":"https://abuelosimpson.github.io/aliran/sdk/","bugs":{"url":"https://github.com/AbueloSimpson/aliran/issues"},"keywords":["p2p","streaming","ott","hls","player","holepunch","hyperswarm","hyperdrive","oprf","aliran"],"engines":{"node":">=20"},"publishConfig":{"access":"public"},"scripts":{"test":"node test.mjs"},"dependencies":{"@aliran/core":"^0.1.1","b4a":"^1.6.6","corestore":"^6.18.0","hyperbee":"^2.20.0","hypercore-crypto":"^3.4.2","hyperdrive":"^11.13.0","hyperswarm":"^4.8.0","protomux-rpc":"^1.5.0","rache":"^1.0.0"},"comment":"Runtime-agnostic: player.js takes injected { http, fs } so it runs in Node (index.js wires node builtins) and in the Bare worklet (client/backend/backend.mjs wires bare-http1/bare-fs). Keep dependency ranges in sync with client/backend/package.json — the Android bundle resolves this same graph. @aliran/core is a semver range for publishability; inside the repo the npm workspace (root package.json) and client/backend's direct file:../../core dep both satisfy it with the local copy — nothing is fetched from the registry.","gitHead":"faa056315a0f4425eb7952991512c17511f3224f","_id":"@aliran/player-sdk@0.1.3","_nodeVersion":"24.17.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-QPRmEZKGAzqovpvmTEXcxvwVYK1xaeRxu5Iomcz9g8gMrAGKsxgotAEPHDUKLcdMPNctjAeM8XHbtsjyFtTPqA==","shasum":"d9ccec0de552353069525ddae070801c51603d40","tarball":"https://registry.npmjs.org/@aliran/player-sdk/-/player-sdk-0.1.3.tgz","fileCount":11,"unpackedSize":186957,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD1Nd6P1E2A0KNbAUpuYewII0P3yzN7mP8VzrDG7MpP+wIhANj1yWjyp/s3TnkSIRi03sV8vmZSQXYwQg2P+9oLc7R5"}]},"_npmUser":{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"},"directories":{},"maintainers":[{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/player-sdk_0.1.3_1785575153265_0.8469904476600574"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-22T02:41:08.522Z","modified":"2026-08-01T09:05:53.561Z","0.1.0":"2026-07-22T02:41:08.882Z","0.1.1":"2026-07-22T04:43:35.064Z","0.1.2":"2026-07-23T03:52:40.627Z","0.1.3":"2026-08-01T09:05:53.408Z"},"bugs":{"url":"https://github.com/AbueloSimpson/aliran/issues"},"license":"MIT","homepage":"https://abuelosimpson.github.io/aliran/sdk/","keywords":["p2p","streaming","ott","hls","player","holepunch","hyperswarm","hyperdrive","oprf","aliran"],"repository":{"type":"git","url":"git+https://github.com/AbueloSimpson/aliran.git","directory":"sdk"},"description":"Headless Aliran player engine — P2P OPRF login, catalog, and localhost HLS serving (Node + Bare)","maintainers":[{"name":"abuelosimpson","email":"abuelo.simpson@nym.hush.com"}],"readme":"# @aliran/player-sdk\r\n\r\nHeadless Aliran player engine — the same core the Android app's Bare worklet\r\nruns, usable from any Node (or Bare) host. It connects to a panel over the DHT,\r\nreplicates the signed catalog DB, performs the OPRF login (no plaintext\r\npassword ever leaves the process), and serves entitled encrypted feeds and\r\ncatalog art on a localhost Range HTTP server that any HLS-capable player can\r\nconsume.\r\n\r\n## Install\r\n\r\n```sh\r\nnpm install @aliran/player-sdk\r\n```\r\n\r\nYou need Node >= 20 (or the Bare runtime — see below). TypeScript definitions\r\nship with the package (`index.d.ts`). Inside the\r\n[Aliran monorepo](https://github.com/AbueloSimpson/aliran) this is the `sdk/`\r\nnpm workspace; the Android app's worklet consumes the very same code via\r\n`file:` (see `client/backend`).\r\n\r\n```js\r\nimport { createPlayer } from '@aliran/player-sdk'\r\n\r\nconst player = createPlayer({ panelPubKey, storeDir: './aliran-store' })\r\nplayer.on('peers', (n) => console.log(n, 'peers'))\r\n\r\nawait player.connect()                    // join the panel topic ('ready')\r\nconst streams = await player.login(user, pass) // display list ('streams'); retry while\r\n                                               // 'not connected to panel' (DHT dialing)\r\nconst { localUrl } = await player.resolve(streams[0].id)\r\n// -> point ffplay / ExoPlayer / hls.js at localUrl (e.g. http://127.0.0.1:PORT/index.m3u8)\r\n```\r\n\r\n## API\r\n\r\nCall `createPlayer(opts)` (Node) or `new AliranPlayer({ ...opts, http, fs })`\r\n(any runtime — inject `node:http`/`node:fs` or `bare-http1`/`bare-fs`):\r\n\r\n| Member | Description |\r\n|---|---|\r\n| `connect(panelPubKey?)` | Join the panel topic + replicate its signed DB. Emits `ready`. |\r\n| `login(username, password)` | OPRF login. Returns/emits the **display list** (id, title, description, category, isLive, `type`/`durationSec`/`status` for VOD titles, poster/backdrop/logo as localhost URLs — stream keys stay inside the engine). Throws `not connected to panel` while the swarm is still dialing: retry. |\r\n| `listStreams()` | Last display list. |\r\n| `resolve(streamId)` | → `{ localUrl, port, feedKey, type, durationSec? }` — replicates the entitled feed (and re-seeds it) and serves it on localhost. `type: 'vod'` = an on-demand library title: a **finished** VOD playlist (seek freely — full Range support — and pause indefinitely, `durationSec` = runtime), served the same way but with **none** of the live self-heal machinery armed (nothing \"advances\" on a finished playlist, so live health checks would false-fire). Build seek/pause UI off `type`, never off the URL shape. |\r\n| `serveFeed(feedKey, encKey)` | Low-level direct-play by raw keys (no login). Returns the port. |\r\n| `assetUrl(path)` | Catalog art path → localhost URL (after login). |\r\n| `stop()` | Full teardown. |\r\n\r\nEvents: `ready` · `streams` (display list — emitted at login, and **re-emitted\r\nlive** whenever the panel edits the catalog: the SDK watches the replicated\r\n`catalog/` range, so title/isLive/art changes push to the host without polling\r\nor re-login; a newly *granted* stream still requires the next login) · `status`\r\n(`{state: 'feed:open'|'feed:ready'}`) · `peers` (count, every 3 s while\r\nserving) · `recovered` (corrupt store purged + retried) · `error` ·\r\n`fallback` (`{streamId, url, reason: 'timeout'|'stall'}`) ·\r\n`source-changed` (`{streamId, source, url}`) ·\r\n`feed-changed` (`{streamId, feedKey, url}` — the stream being watched had its\r\n`feedKey` rotated in the catalog (broadcaster source change / RAM restart);\r\nthe SDK re-resolved and swapped the served feed behind the **same** localhost\r\n`url`, so the host just reloads the player to flush the stale playlist — no\r\nre-login or `resolve()` needed). The emitter never throws on unhandled `error`.\r\n\r\n## Redirect channels — the CDN path\r\n\r\nA catalog entry can be a **redirect channel** instead of a P2P feed: the admin\r\npanel stores `{ redirect: true, url: 'https://…' }` on the record, and\r\n`resolve()` returns that URL verbatim with `source: 'cdn'` and **no `port`** —\r\nno feed open, no swarm join, no watchdogs. The host player fetches the URL\r\ndirectly (any HLS the platform player supports), and its errors are the host's\r\nto surface. Because the URL rides the replicated catalog, an admin edit\r\nreaches viewers on their **next tune** — no re-login. Entitlement is\r\nunchanged: the channel appears only for granted users.\r\n\r\nThis is the **only** CDN mechanism in the product. A channel is either P2P\r\n(kept playing by the tune self-heal ladder) or a redirect — **P2P channels\r\nhave no CDN failover, by design**.\r\n\r\n## Hybrid mode (internal — test harness only)\r\n\r\nThe engine retains a config-driven `hybrid` option\r\n(`mode: 'p2p-only'|'hybrid'|'cdn-only'`, a global `cdnUrl` template, and the\r\n`fallback` / `source-changed` events) from before redirect channels existed.\r\nIt is **not a product path** — the app never configures it — and it survives\r\nas infrastructure for the e2e harness (`test:sdk` uses it to prove the\r\nserving-health verdicts). Leave it unset: the default `p2p-only` is the\r\nshipped behavior.\r\n\r\n## Zap latency\r\n\r\nThe localhost server (`serve.js`, shared with the desktop tools) is tuned for\r\nfast channel switching: segment bodies stream **block-progressively** (bytes\r\nreach the player as they replicate — no waiting for the full blob), a\r\nnot-yet-replicated playlist/segment request is **held briefly and served on\r\narrival** instead of 404ing, and each playlist request **read-aheads the\r\nnewest segments in parallel**. Two warm-up options stack on top:\r\n\r\n- `prewarm` — open entitled feeds' DHT topics right after login so the *first*\r\n  zap is warm. `false` (default) | `true` (all) | integer cap (lowest curated\r\n  order first). Bandwidth-cheap: it warms connections, not downloads.\r\n- `zapPrefetch` — while a stream plays, keep the **newest segment** of the\r\n  next/previous channels in curated zap order replicated locally, so CH+/CH−\r\n  starts from warm bytes. **Off by default — costs standing bandwidth**\r\n  (≈ each neighbor's full bitrate while playing). `true` uses the adaptive\r\n  defaults below, or pass an object to tune `{ neighbors, intervalMs,\r\n  directional, stallMs, resumeMs, minHeadroom }`.\r\n\r\n### Smooth zapping: runtime toggle + adaptive gate\r\n\r\n`zapPrefetch` is designed to be a **user-facing choice** (the app surfaces it\r\nas \"Smooth zapping — uses more data\"):\r\n\r\n- **Runtime switch** — `player.setZapPrefetch(true | false | cfg)` applies\r\n  mid-play: OFF stops the warm loop and drops every standing download\r\n  instantly; ON re-arms against the active stream. This is echoed as a\r\n  `'zap-prefetch'` `{enabled}` event.\r\n- **Adaptive gate** — prefetch must never compete with playback or surprise\r\n  someone on a paid connection, so the engine suspends the warm loop\r\n  (dropping its downloads, keeping the tick alive to observe recovery)\r\n  whenever:\r\n  - the host reports a **metered/expensive network** via\r\n    `player.setNetworkProfile({ expensive })` (this lifts the moment it is\r\n    cheap again);\r\n  - the **active playlist stops advancing** for `stallMs` (default 12 s — the\r\n    viewer's own stream is starving), and resumes after `resumeMs` (default\r\n    60 s) of clean advance;\r\n  - neighbor segments download **slower than `minHeadroom`× realtime**\r\n    (default 3×, two thin samples in a row) — the pipe has no room for a\r\n    second stream.\r\n  Suspensions/resumes surface as `'zap-prefetch'` `{state:'suspended',reason}`\r\n  / `{state:'resumed'}` events (`reason: 'metered' | 'stall' | 'thin'`).\r\n- **Directional** (`directional: true`, the default) — once the viewer's surf\r\n  direction is known (an adjacent-channel move), the engine warms only that\r\n  side, halving the standing cost for the common CH+/CH+/CH+ pattern; a menu\r\n  jump resets to both sides. The channel just left stays warm in the feed\r\n  cache regardless.\r\n\r\n## Upload policy\r\n\r\n`createPlayer({ uploadPolicy: 'reseed' | 'client-only' })` — `'reseed'`\r\n(default) joins feed/assets topics announced (`server: true`): other viewers\r\ncan request the blocks this viewer replicated (opportunistic, demand-driven\r\nupload that strengthens the swarm). `'client-only'` joins **unannounced**\r\n(`server: false`): the peer is not discoverable on those topics, so other\r\nviewers can never dial it — this gives practically **zero viewer-to-viewer\r\nupload** by construction, at the swarm-wide cost of one fewer re-seeder. It is\r\na boot-time option; `setUploadPolicy()` switches it live (re-joins the active\r\ntopics and drops standing reseed connections without blipping playback). See\r\nthe [viewer bandwidth page](https://abuelosimpson.github.io/aliran/kb/viewer-bandwidth/)\r\nfor measured numbers.\r\n\r\n## Swarm tuning\r\n\r\n`createPlayer({ swarm: { maxPeers } })` raises the total-connection budget of\r\nthe engine's single Hyperswarm (lib default 64 — plenty for a viewer).\r\nOrdinary viewers should omit it; SDK-based **seed nodes** and the repeater\r\nappliance raise it into the hundreds so they can hold big fan-out while\r\nre-seeding.\r\n\r\n`swarm: { bootstrap: [{ host, port }, …] }` points the engine at custom DHT\r\nbootstrap nodes — for local DHT testnets (`hyperdht/testnet.js`, used by\r\n`test:repeater`) or private-DHT deployments. Omit it for the public DHT.\r\n\r\n**UDP socket buffers** (`swarm: { rcvbufMb, sndbufMb }`, MiB): all peer streams\r\nmultiplex over the engine's **one UDP socket pair**, so when a socket buffer\r\noverflows, the kernel drops datagrams silently and playback stalls with\r\nnothing in any log. By default the engine requests a **2 MiB receive buffer**\r\n(a viewer is download-dominant — the whole stream funnels into the receive\r\nside while the JS thread is busy decrypting) and leaves **send untouched**\r\n(reseed upload is opportunistic and never buffer-bound on a typical uplink).\r\n`0` disables a direction; raise `sndbufMb` on SDK-based seed nodes. Semantics\r\nmirror the server envs `SWARM_RCVBUF_MB`/`SWARM_SNDBUF_MB` (see the\r\n[network tuning page](https://abuelosimpson.github.io/aliran/kb/network-tuning/)).\r\nThis is best-effort everywhere: on hosts where `/proc` is unreadable (Android,\r\nWindows, macOS) the request still applies — only clamp *detection* degrades —\r\nand the outcome is emitted as a `status` event `{ state: 'net:tuned', message }`.\r\n\r\nThe on-disk store is a **disposable replica cache**: the engine detects\r\ncorruption (e.g. a crash mid-write → `OPLOG_CORRUPT`), purges the store, and\r\nretries the operation once — in-memory entitlements survive, and everything\r\nre-replicates from peers (`recover.js`, verified by `npm run test:corrupt`).\r\n\r\n**Partial adoption:** you can keep your own catalog/metadata and use only\r\n`login()` + `resolve()` for the video URL — video travels P2P, metadata stays\r\nyours.\r\n\r\n## Layout\r\n\r\n- `player.js` — runtime-agnostic engine (`{ http, fs }` injected; no Node/Bare imports)\r\n- `index.js` — Node entry (wires `node:http`/`node:fs`; exports `createPlayer`)\r\n- `login.js` — OPRF login protocol (canonical home; `client/backend/login.mjs` re-exports)\r\n- `recover.js` — store-corruption recovery (canonical home)\r\n- `serve.js` — progressive media-serving core (availability wait, Range, read-ahead;\r\n  also behind `tools/lib/serve-drive.js`)\r\n\r\nThe app's worklet (`client/backend/backend.mjs`) is a thin IPC shell over `player.js`.\r\n\r\n## Tests\r\n\r\n- `npm test` (from `sdk/`) — fast unit tests, no network.\r\n- `npm run test:sdk` (repo root) — headless e2e: real panel + broadcaster, SDK\r\n  login → resolve → ffprobe-validated HLS over P2P. Needs ffmpeg/ffprobe on PATH.\r\n- `npm run test:serve` (repo root) — deterministic serving-core test: progressive\r\n  first-byte-before-full-blob, availability wait, Range math, playlist read-ahead.\r\n\r\nFor React Native apps, see\r\n**[`@aliran/react-native`](https://github.com/AbueloSimpson/aliran/tree/main/sdk/react-native)** —\r\na drop-in `<AliranVideo>` component + worklet host built on this engine. For native\r\n(non-RN) Android apps, see\r\n**[`aliran-kit`](https://github.com/AbueloSimpson/aliran/tree/main/sdk/android)** — the\r\nKotlin twin (same engine + player contracts, one APK from Android 5.0, P2P on 10+).\r\nA runnable headless example lives in\r\n[`examples/headless-player.mjs`](https://github.com/AbueloSimpson/aliran/tree/main/examples).\r\n","readmeFilename":"README.md"}