Add media-streaming category with 20 P2P streaming modules.
Introduces peer-assisted live/VOD primitives (chunking, manifests, crazy trees, bandwidth aggregation, FEC, enterprise controls) with shared media-streaming-base, registry entries, and brittle tests. Four core modules ship as production tier; sixteen as scaffold for deepen passes. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# Media streaming
|
||||
|
||||
**Path:** `modules/media-streaming/` · **Modules:** 20
|
||||
|
||||
Peer-assisted, bandwidth-pooled, tree/mesh-hybrid **live + VOD** streaming. A viewer maintains **one logical high-quality stream**; the overlay (crazy trees + helper swarms) amplifies bandwidth through cooperative peers.
|
||||
|
||||
Shared types: [`../_shared/media-streaming-base.js`](../_shared/media-streaming-base.js)
|
||||
|
||||
## Philosophy
|
||||
|
||||
| Principle | Modules |
|
||||
|-----------|---------|
|
||||
| One logical stream | `bandwidth-aggregator`, `helper-swarm-coordinator`, `stream-manifest` |
|
||||
| Crazy trees | `media-tree-orchestrator`, `peer-selector-streaming`, `latency-optimizer` |
|
||||
| Extreme quality (4K@144+) | `quality-ladder`, `chunk-scheduler-media`, `fec-video`, `adaptive-streaming-engine` |
|
||||
| Enterprise | `stream-access-control`, `stream-telemetry`, `origin-hybrid-bridge`, `enterprise-orchestrator`, `content-protection` |
|
||||
|
||||
## Tiers
|
||||
|
||||
| Tier | Modules |
|
||||
|------|---------|
|
||||
| **production** | `media-tree-orchestrator`, `peer-selector-streaming`, `bandwidth-aggregator`, `chunk-scheduler-media` |
|
||||
| **scaffold** | remaining 16 (full API + tests; deepen in later waves) |
|
||||
|
||||
## Composition
|
||||
|
||||
Typical stack:
|
||||
|
||||
```text
|
||||
media-chunker → stream-manifest + quality-ladder
|
||||
media-tree-orchestrator → peer-selector-streaming → helper-swarm-coordinator
|
||||
chunk-scheduler-media → bandwidth-aggregator → buffer-health-predictor
|
||||
fec-video + retransmission-media + adaptive-streaming-engine
|
||||
contribution-ledger + stream-telemetry + stream-access-control
|
||||
```
|
||||
|
||||
Registry `composes_with` links to `network-stack`, `core-infrastructure`, `scheduling-queues`, `measurement-rate-control`, and `trust-security`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
const { HyperP2PMediaChunker } = require('hyper-p2p-media-chunker')
|
||||
const { HyperP2PBandwidthAggregator } = require('hyper-p2p-bandwidth-aggregator')
|
||||
const { HyperP2PMediaTreeOrchestrator } = require('hyper-p2p-media-tree-orchestrator')
|
||||
|
||||
const chunker = new HyperP2PMediaChunker({ chunkSize: 256 * 1024 })
|
||||
const tree = new HyperP2PMediaTreeOrchestrator({ maxFanout: 8 })
|
||||
const agg = new HyperP2PBandwidthAggregator({ streamId: 'live-1' })
|
||||
|
||||
const chunks = chunker.segment(Buffer.from('...'), { streamId: 'live-1', keyframe: true })
|
||||
for (const c of chunks) agg.ingest(c.seq, 'peer-a', c.data)
|
||||
const view = agg.logicalView()
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
cd hyper-p2p-bandwidth-aggregator && npm test
|
||||
for d in hyper-p2p-*/; do (cd "$d" && npm test) || exit 1; done
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-adaptive-streaming-engine
|
||||
|
||||
**Protocol:** `adaptive-streaming-engine/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-adaptive-streaming-engine
|
||||
|
||||
**Protocol:** `adaptive-streaming-engine/v1`
|
||||
|
||||
**Export:** `{ HyperP2PAdaptiveStreamingEngine, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PAdaptiveStreamingEngine` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'adaptive-streaming-engine/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-adaptive-streaming-engine
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PAdaptiveStreamingEngine } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PAdaptiveStreamingEngine()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'adaptive-streaming-engine/v1'
|
||||
|
||||
class HyperP2PAdaptiveStreamingEngine extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.targetFps = opts.targetFps ?? 144
|
||||
this._bufferMs = 0
|
||||
this._swarmHealth = 1
|
||||
this._recommendation = { bitrate: 0, repId: 'auto', reason: 'init' }
|
||||
this._stats = { reports: 0, adjustments: 0 }
|
||||
}
|
||||
|
||||
reportBuffer (levelMs) {
|
||||
this._bufferMs = Math.max(0, levelMs | 0)
|
||||
this._stats.reports++
|
||||
this._recompute()
|
||||
return this._recommendation
|
||||
}
|
||||
|
||||
reportSwarmHealth (score) {
|
||||
this._swarmHealth = Math.min(1, Math.max(0, Number(score) || 0))
|
||||
this._recompute()
|
||||
return this._recommendation
|
||||
}
|
||||
|
||||
_recompute () {
|
||||
const health = this._swarmHealth
|
||||
const buffer = this._bufferMs
|
||||
let bitrate = 5_000_000
|
||||
if (buffer < 500) bitrate = 1_000_000
|
||||
else if (buffer < 2000) bitrate = 8_000_000
|
||||
else bitrate = 25_000_000
|
||||
bitrate = Math.floor(bitrate * health)
|
||||
const repId = bitrate >= 20_000_000 ? '4k144' : bitrate >= 8_000_000 ? '4k60' : 'hd'
|
||||
const prev = this._recommendation.bitrate
|
||||
this._recommendation = {
|
||||
bitrate,
|
||||
repId,
|
||||
fps: this.targetFps,
|
||||
reason: buffer < 500 ? 'low-buffer' : health < 0.5 ? 'swarm-stress' : 'max-quality'
|
||||
}
|
||||
if (prev !== bitrate) {
|
||||
this._stats.adjustments++
|
||||
this.emit('recommend', this._recommendation)
|
||||
}
|
||||
}
|
||||
|
||||
recommendBitrate (swarmHealth = null) {
|
||||
if (swarmHealth != null) this.reportSwarmHealth(swarmHealth)
|
||||
return this._recommendation
|
||||
}
|
||||
|
||||
currentRecommendation () {
|
||||
return { ...this._recommendation, bufferMs: this._bufferMs, swarmHealth: this._swarmHealth }
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { recommendation: this._recommendation })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PAdaptiveStreamingEngine, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-adaptive-streaming-engine",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — adaptive streaming engine.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PAdaptiveStreamingEngine, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PAdaptiveStreamingEngine); t.is(PROTOCOL, 'adaptive-streaming-engine/v1') })
|
||||
|
||||
test('recommend', async (t) => {
|
||||
const m = new HyperP2PAdaptiveStreamingEngine()
|
||||
m.reportBuffer(3000)
|
||||
m.reportSwarmHealth(0.9)
|
||||
t.ok(m.currentRecommendation().bitrate > 0)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-bandwidth-aggregator
|
||||
|
||||
**Protocol:** `bandwidth-aggregator/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-bandwidth-aggregator
|
||||
|
||||
**Protocol:** `bandwidth-aggregator/v1`
|
||||
|
||||
**Export:** `{ HyperP2PBandwidthAggregator, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PBandwidthAggregator` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'bandwidth-aggregator/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-bandwidth-aggregator
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PBandwidthAggregator } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PBandwidthAggregator()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const { assertStreamId, chunkKey, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'bandwidth-aggregator/v1'
|
||||
|
||||
class HyperP2PBandwidthAggregator extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.streamId = assertStreamId(opts.streamId || 'default')
|
||||
this._chunks = new Map()
|
||||
this._sources = new Map()
|
||||
this._nextSeq = 0
|
||||
this._stats = { ingested: 0, duplicates: 0, bytes: 0 }
|
||||
}
|
||||
|
||||
ingest (seq, peerId, data) {
|
||||
const key = chunkKey(this.streamId, seq)
|
||||
if (this._chunks.has(key)) {
|
||||
this._stats.duplicates++
|
||||
return false
|
||||
}
|
||||
const buf = b4a.isBuffer(data) ? data : b4a.from(data)
|
||||
this._chunks.set(key, { seq, peerId, data: buf, at: Date.now() })
|
||||
const src = this._sources.get(peerId) || { peerId, chunks: 0, bytes: 0 }
|
||||
src.chunks++
|
||||
src.bytes += buf.length
|
||||
this._sources.set(peerId, src)
|
||||
this._nextSeq = Math.max(this._nextSeq, seq + 1)
|
||||
this._stats.ingested++
|
||||
this._stats.bytes += buf.length
|
||||
this.emit('ingest', { seq, peerId, bytes: buf.length })
|
||||
return true
|
||||
}
|
||||
|
||||
hasSeq (seq) {
|
||||
return this._chunks.has(chunkKey(this.streamId, seq))
|
||||
}
|
||||
|
||||
missingRanges () {
|
||||
const seqs = [...this._chunks.values()].map((c) => c.seq).sort((a, b) => a - b)
|
||||
if (!seqs.length) return [{ from: 0, to: 0 }]
|
||||
const gaps = []
|
||||
for (let i = 1; i < seqs.length; i++) {
|
||||
if (seqs[i] - seqs[i - 1] > 1) {
|
||||
gaps.push({ from: seqs[i - 1] + 1, to: seqs[i] - 1 })
|
||||
}
|
||||
}
|
||||
return gaps
|
||||
}
|
||||
|
||||
logicalView (maxSeq = null) {
|
||||
const limit = maxSeq == null ? this._nextSeq : maxSeq
|
||||
const parts = []
|
||||
for (let s = 0; s < limit; s++) {
|
||||
const c = this._chunks.get(chunkKey(this.streamId, s))
|
||||
if (c) parts.push(c.data)
|
||||
}
|
||||
return parts.length ? b4a.concat(parts) : null
|
||||
}
|
||||
|
||||
sourceStats () {
|
||||
return [...this._sources.values()]
|
||||
}
|
||||
|
||||
coverage () {
|
||||
if (!this._nextSeq) return 0
|
||||
return this._chunks.size / this._nextSeq
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, {
|
||||
streamId: this.streamId,
|
||||
chunks: this._chunks.size,
|
||||
coverage: this.coverage(),
|
||||
sources: this._sources.size
|
||||
})
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._chunks.clear()
|
||||
this._sources.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PBandwidthAggregator, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-bandwidth-aggregator",
|
||||
"version": "0.3.1",
|
||||
"description": "P2P media streaming — bandwidth aggregator.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PBandwidthAggregator, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PBandwidthAggregator); t.is(PROTOCOL, 'bandwidth-aggregator/v1') })
|
||||
|
||||
test('aggregate chunks', async (t) => {
|
||||
const m = new HyperP2PBandwidthAggregator({ streamId: 's1' })
|
||||
t.ok(m.ingest(0, 'p1', Buffer.from('aa')))
|
||||
t.ok(m.ingest(1, 'p2', Buffer.from('bb')))
|
||||
const view = m.logicalView()
|
||||
t.is(view.toString(), 'aabb')
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-buffer-health-predictor
|
||||
|
||||
**Protocol:** `buffer-health-predictor/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-buffer-health-predictor
|
||||
|
||||
**Protocol:** `buffer-health-predictor/v1`
|
||||
|
||||
**Export:** `{ HyperP2PBufferHealthPredictor, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PBufferHealthPredictor` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'buffer-health-predictor/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-buffer-health-predictor
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PBufferHealthPredictor } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PBufferHealthPredictor()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'buffer-health-predictor/v1'
|
||||
|
||||
class HyperP2PBufferHealthPredictor extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this._samples = []
|
||||
this.maxSamples = opts.maxSamples ?? 64
|
||||
this._topologyScore = 1
|
||||
this._contribScore = 1
|
||||
this._stats = { samples: 0, warnings: 0 }
|
||||
}
|
||||
|
||||
reportBuffer (levelMs) {
|
||||
const v = Math.max(0, levelMs | 0)
|
||||
this._samples.push({ levelMs: v, at: Date.now() })
|
||||
if (this._samples.length > this.maxSamples) this._samples.shift()
|
||||
this._stats.samples++
|
||||
const risk = this.predictStallRisk()
|
||||
if (risk > 0.7) {
|
||||
this._stats.warnings++
|
||||
this.emit('stall-risk', { risk, levelMs: v })
|
||||
}
|
||||
return risk
|
||||
}
|
||||
|
||||
setSignals ({ topologyScore, contribScore } = {}) {
|
||||
if (topologyScore != null) this._topologyScore = Math.min(1, Math.max(0, topologyScore))
|
||||
if (contribScore != null) this._contribScore = Math.min(1, Math.max(0, contribScore))
|
||||
}
|
||||
|
||||
predictStallRisk () {
|
||||
if (!this._samples.length) return 0
|
||||
const recent = this._samples.slice(-8)
|
||||
const avg = recent.reduce((s, x) => s + x.levelMs, 0) / recent.length
|
||||
const trend = recent.length > 1
|
||||
? recent[recent.length - 1].levelMs - recent[0].levelMs
|
||||
: 0
|
||||
let risk = avg < 800 ? 0.8 : avg < 2000 ? 0.35 : 0.1
|
||||
if (trend < -200) risk = Math.min(1, risk + 0.25)
|
||||
risk = risk * (2 - this._topologyScore) * (2 - this._contribScore)
|
||||
return Math.min(1, Math.max(0, risk))
|
||||
}
|
||||
|
||||
criticalChunksNeeded () {
|
||||
const risk = this.predictStallRisk()
|
||||
if (risk < 0.4) return []
|
||||
return [{ type: 'keyframe', count: risk > 0.7 ? 3 : 1 }, { type: 'audio', count: 1 }]
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, {
|
||||
stallRisk: this.predictStallRisk(),
|
||||
sampleCount: this._samples.length
|
||||
})
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._samples = []
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PBufferHealthPredictor, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-buffer-health-predictor",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — buffer health predictor.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PBufferHealthPredictor, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PBufferHealthPredictor); t.is(PROTOCOL, 'buffer-health-predictor/v1') })
|
||||
|
||||
test('stall risk', async (t) => {
|
||||
const m = new HyperP2PBufferHealthPredictor()
|
||||
m.reportBuffer(400)
|
||||
t.ok(m.predictStallRisk() > 0.5)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-chunk-scheduler-media
|
||||
|
||||
**Protocol:** `chunk-scheduler-media/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-chunk-scheduler-media
|
||||
|
||||
**Protocol:** `chunk-scheduler-media/v1`
|
||||
|
||||
**Export:** `{ HyperP2PChunkSchedulerMedia, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PChunkSchedulerMedia` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'chunk-scheduler-media/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-chunk-scheduler-media
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PChunkSchedulerMedia } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PChunkSchedulerMedia()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { CHUNK_TYPES, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'chunk-scheduler-media/v1'
|
||||
|
||||
const PRIORITY = {
|
||||
[CHUNK_TYPES.KEYFRAME]: 100,
|
||||
[CHUNK_TYPES.BASE]: 80,
|
||||
[CHUNK_TYPES.AUDIO]: 70,
|
||||
[CHUNK_TYPES.ENHANCEMENT]: 50,
|
||||
[CHUNK_TYPES.DELTA]: 40
|
||||
}
|
||||
|
||||
class HyperP2PChunkSchedulerMedia extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this._queue = []
|
||||
this._stats = { enqueued: 0, scheduled: 0 }
|
||||
}
|
||||
|
||||
enqueue (chunk, priority = null) {
|
||||
if (!chunk || chunk.seq == null) throw new Error('chunk with seq required')
|
||||
const p = priority != null ? priority : (PRIORITY[chunk.type] ?? 30)
|
||||
const entry = { chunk, priority: p, enqueuedAt: Date.now() }
|
||||
this._queue.push(entry)
|
||||
this._stats.enqueued++
|
||||
this._sort()
|
||||
this.emit('enqueue', entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
_sort () {
|
||||
this._queue.sort((a, b) => b.priority - a.priority || a.chunk.seq - b.chunk.seq)
|
||||
}
|
||||
|
||||
nextChunks (limit = 8) {
|
||||
const out = this._queue.splice(0, Math.max(0, limit | 0))
|
||||
this._stats.scheduled += out.length
|
||||
if (out.length) this.emit('schedule', { count: out.length })
|
||||
return out.map((e) => e.chunk)
|
||||
}
|
||||
|
||||
prioritizeKeyframes () {
|
||||
for (const e of this._queue) {
|
||||
if (e.chunk.type === CHUNK_TYPES.KEYFRAME || e.chunk.keyframe) {
|
||||
e.priority = PRIORITY[CHUNK_TYPES.KEYFRAME]
|
||||
}
|
||||
}
|
||||
this._sort()
|
||||
return this._queue.length
|
||||
}
|
||||
|
||||
schedulePlan () {
|
||||
const plan = { keyframes: 0, audio: 0, enhancement: 0, other: 0 }
|
||||
for (const e of this._queue) {
|
||||
if (e.chunk.type === CHUNK_TYPES.KEYFRAME) plan.keyframes++
|
||||
else if (e.chunk.type === CHUNK_TYPES.AUDIO) plan.audio++
|
||||
else if (e.chunk.type === CHUNK_TYPES.ENHANCEMENT) plan.enhancement++
|
||||
else plan.other++
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
pendingCount () { return this._queue.length }
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { pending: this._queue.length })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._queue = []
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PChunkSchedulerMedia, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-chunk-scheduler-media",
|
||||
"version": "0.3.1",
|
||||
"description": "P2P media streaming — chunk scheduler media.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PChunkSchedulerMedia, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PChunkSchedulerMedia); t.is(PROTOCOL, 'chunk-scheduler-media/v1') })
|
||||
|
||||
test('priority schedule', async (t) => {
|
||||
const m = new HyperP2PChunkSchedulerMedia()
|
||||
m.enqueue({ seq: 2, type: 'delta' })
|
||||
m.enqueue({ seq: 0, type: 'keyframe', keyframe: true })
|
||||
const next = m.nextChunks(1)
|
||||
t.is(next[0].type, 'keyframe')
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-content-protection
|
||||
|
||||
**Protocol:** `content-protection/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-content-protection
|
||||
|
||||
**Protocol:** `content-protection/v1`
|
||||
|
||||
**Export:** `{ HyperP2PContentProtection, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PContentProtection` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'content-protection/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-content-protection
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PContentProtection } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PContentProtection()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const crypto = require('hypercore-crypto')
|
||||
const { assertStreamId, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'content-protection/v1'
|
||||
|
||||
class HyperP2PContentProtection extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this._keys = new Map()
|
||||
this._activeKeyId = opts.keyId || 'default'
|
||||
this._stats = { encrypted: 0, decrypted: 0, rotations: 0 }
|
||||
this._ensureKey(this._activeKeyId)
|
||||
}
|
||||
|
||||
_ensureKey (keyId) {
|
||||
if (!this._keys.has(keyId)) {
|
||||
this._keys.set(keyId, crypto.hash(b4a.from('media-key:' + keyId)))
|
||||
}
|
||||
return this._keys.get(keyId)
|
||||
}
|
||||
|
||||
encryptSegment (payload, keyId = null) {
|
||||
if (payload == null) throw new Error('payload required')
|
||||
const kid = keyId || this._activeKeyId
|
||||
const key = this._ensureKey(kid)
|
||||
const buf = b4a.isBuffer(payload) ? payload : b4a.from(payload)
|
||||
const out = b4a.alloc(buf.length)
|
||||
for (let i = 0; i < buf.length; i++) out[i] = buf[i] ^ key[i % key.length]
|
||||
this._stats.encrypted++
|
||||
return { keyId: kid, data: out, at: Date.now() }
|
||||
}
|
||||
|
||||
decryptSegment (payload, keyId = null) {
|
||||
const res = this.encryptSegment(payload, keyId)
|
||||
this._stats.decrypted++
|
||||
return res.data
|
||||
}
|
||||
|
||||
rotateKeys (newKeyId = null) {
|
||||
const kid = newKeyId || b4a.toString(crypto.hash(b4a.from(String(Date.now()))), 'hex').slice(0, 12)
|
||||
this._activeKeyId = kid
|
||||
this._ensureKey(kid)
|
||||
this._stats.rotations++
|
||||
this.emit('rotate', { keyId: kid })
|
||||
return kid
|
||||
}
|
||||
|
||||
bindStream (streamId, keyId = null) {
|
||||
const sid = assertStreamId(streamId)
|
||||
const kid = keyId || this._activeKeyId
|
||||
this._ensureKey(kid)
|
||||
return { streamId: sid, keyId: kid }
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { activeKeyId: this._activeKeyId, keys: this._keys.size })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._keys.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PContentProtection, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-content-protection",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — content protection.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PContentProtection, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PContentProtection); t.is(PROTOCOL, 'content-protection/v1') })
|
||||
|
||||
test('encrypt decrypt', async (t) => {
|
||||
const m = new HyperP2PContentProtection()
|
||||
const enc = m.encryptSegment(Buffer.from('secret'))
|
||||
const dec = m.decryptSegment(enc.data, enc.keyId)
|
||||
t.is(dec.toString(), 'secret')
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-contribution-ledger
|
||||
|
||||
**Protocol:** `contribution-ledger/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-contribution-ledger
|
||||
|
||||
**Protocol:** `contribution-ledger/v1`
|
||||
|
||||
**Export:** `{ HyperP2PContributionLedger, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PContributionLedger` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'contribution-ledger/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-contribution-ledger
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PContributionLedger } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PContributionLedger()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
|
||||
const { assertPeerId, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'contribution-ledger/v1'
|
||||
|
||||
class HyperP2PContributionLedger extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.topic = opts.topic || null
|
||||
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||
this._ledger = new Map()
|
||||
this._stats = { recorded: 0, gossipOut: 0 }
|
||||
this.swarm = null
|
||||
this._peerMsgs = null
|
||||
}
|
||||
|
||||
recordUpload (peerId, bytes, streamId = null) {
|
||||
const id = assertPeerId(peerId)
|
||||
const n = Math.max(0, bytes | 0)
|
||||
const cur = this._ledger.get(id) || { peerId: id, bytes: 0, streams: new Set() }
|
||||
cur.bytes += n
|
||||
if (streamId) cur.streams.add(String(streamId))
|
||||
cur.updatedAt = Date.now()
|
||||
this._ledger.set(id, cur)
|
||||
this._stats.recorded++
|
||||
this._gossip({ type: 'contrib', peerId: id, bytes: n, streamId })
|
||||
this.emit('upload', { peerId: id, bytes: n })
|
||||
return cur.bytes
|
||||
}
|
||||
|
||||
balance (peerId) {
|
||||
const cur = this._ledger.get(assertPeerId(peerId))
|
||||
return cur ? cur.bytes : 0
|
||||
}
|
||||
|
||||
topContributors (limit = 10) {
|
||||
return [...this._ledger.values()]
|
||||
.sort((a, b) => b.bytes - a.bytes)
|
||||
.slice(0, Math.max(0, limit | 0))
|
||||
.map((r) => ({ peerId: r.peerId, bytes: r.bytes }))
|
||||
}
|
||||
|
||||
_gossip (payload) {
|
||||
if (this._peerMsgs) {
|
||||
gossipSend(this, payload)
|
||||
this._stats.gossipOut++
|
||||
}
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { peers: this._ledger.size })
|
||||
}
|
||||
|
||||
async ready () {
|
||||
if (this.swarm || !this.topic) return this
|
||||
await initModuleSwarm(this, {
|
||||
keyPair: this.keyPair,
|
||||
topic: this.topic,
|
||||
protocol: PROTOCOL,
|
||||
onmessage: (d) => {
|
||||
if (d?.type === 'contrib') this.recordUpload(d.peerId, d.bytes, d.streamId)
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||
this.swarm = null
|
||||
this._ledger.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PContributionLedger, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-contribution-ledger",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — contribution ledger.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PContributionLedger, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PContributionLedger); t.is(PROTOCOL, 'contribution-ledger/v1') })
|
||||
|
||||
test('record upload', async (t) => {
|
||||
const m = new HyperP2PContributionLedger()
|
||||
m.recordUpload('peer-a', 1000)
|
||||
t.is(m.balance('peer-a'), 1000)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-enterprise-orchestrator
|
||||
|
||||
**Protocol:** `enterprise-orchestrator/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-enterprise-orchestrator
|
||||
|
||||
**Protocol:** `enterprise-orchestrator/v1`
|
||||
|
||||
**Export:** `{ HyperP2PEnterpriseOrchestrator, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PEnterpriseOrchestrator` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'enterprise-orchestrator/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-enterprise-orchestrator
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PEnterpriseOrchestrator } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PEnterpriseOrchestrator()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'enterprise-orchestrator/v1'
|
||||
|
||||
class HyperP2PEnterpriseOrchestrator extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this._regions = new Map()
|
||||
this._sla = { targetUptime: opts.targetUptime ?? 0.999, maxStallRate: opts.maxStallRate ?? 0.01 }
|
||||
this._stats = { deploys: 0, violations: 0 }
|
||||
}
|
||||
|
||||
deployRegion (region, seedPeers = []) {
|
||||
if (!region) throw new Error('region required')
|
||||
const rec = {
|
||||
region: String(region),
|
||||
seedPeers: seedPeers.map(String),
|
||||
deployedAt: Date.now(),
|
||||
trees: 0,
|
||||
viewers: 0
|
||||
}
|
||||
this._regions.set(rec.region, rec)
|
||||
this._stats.deploys++
|
||||
this.emit('deploy', rec)
|
||||
return rec
|
||||
}
|
||||
|
||||
updateRegionStats (region, patch = {}) {
|
||||
const r = this._regions.get(String(region))
|
||||
if (!r) return false
|
||||
Object.assign(r, patch)
|
||||
return true
|
||||
}
|
||||
|
||||
slaStatus () {
|
||||
let ok = true
|
||||
const regions = [...this._regions.values()]
|
||||
for (const r of regions) {
|
||||
if (r.stallRate != null && r.stallRate > this._sla.maxStallRate) {
|
||||
ok = false
|
||||
this._stats.violations++
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok,
|
||||
regions: regions.length,
|
||||
sla: { ...this._sla },
|
||||
violations: this._stats.violations
|
||||
}
|
||||
}
|
||||
|
||||
coordinateSwarm (config = {}) {
|
||||
const plan = {
|
||||
regions: [...this._regions.keys()],
|
||||
maxFanout: config.maxFanout ?? 8,
|
||||
seedPeers: config.seedPeers || [],
|
||||
at: Date.now()
|
||||
}
|
||||
this.emit('coordinate', plan)
|
||||
return plan
|
||||
}
|
||||
|
||||
listRegions () {
|
||||
return [...this._regions.values()]
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { regions: this._regions.size })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._regions.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PEnterpriseOrchestrator, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-enterprise-orchestrator",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — enterprise orchestrator.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PEnterpriseOrchestrator, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PEnterpriseOrchestrator); t.is(PROTOCOL, 'enterprise-orchestrator/v1') })
|
||||
|
||||
test('deploy region', async (t) => {
|
||||
const m = new HyperP2PEnterpriseOrchestrator()
|
||||
m.deployRegion('us-east', ['seed-1'])
|
||||
t.ok(m.slaStatus().ok)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-fec-video
|
||||
|
||||
**Protocol:** `fec-video/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-fec-video
|
||||
|
||||
**Protocol:** `fec-video/v1`
|
||||
|
||||
**Export:** `{ HyperP2PFecVideo, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PFecVideo` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'fec-video/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-fec-video
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PFecVideo } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PFecVideo()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const crypto = require('hypercore-crypto')
|
||||
const { mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'fec-video/v1'
|
||||
|
||||
class HyperP2PFecVideo extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.redundancy = opts.redundancy ?? 0.25
|
||||
this._groups = new Map()
|
||||
this._stats = { encoded: 0, recovered: 0 }
|
||||
}
|
||||
|
||||
encodeGroup (groupId, shards) {
|
||||
if (!Array.isArray(shards) || !shards.length) throw new Error('shards required')
|
||||
const gid = String(groupId)
|
||||
const dataShards = shards.map((s) => (b4a.isBuffer(s) ? s : b4a.from(s)))
|
||||
const parityCount = Math.max(1, Math.ceil(dataShards.length * this.redundancy))
|
||||
const parities = []
|
||||
for (let i = 0; i < parityCount; i++) {
|
||||
let acc = b4a.alloc(dataShards[0].length)
|
||||
for (let j = 0; j < dataShards.length; j++) {
|
||||
const mix = crypto.hash(b4a.concat([dataShards[j], b4a.from(String(i + j))]))
|
||||
for (let k = 0; k < acc.length; k++) acc[k] ^= mix[k % mix.length]
|
||||
}
|
||||
parities.push(acc)
|
||||
}
|
||||
const group = {
|
||||
groupId: gid,
|
||||
dataCount: dataShards.length,
|
||||
parityCount,
|
||||
shards: dataShards,
|
||||
parities,
|
||||
at: Date.now()
|
||||
}
|
||||
this._groups.set(gid, group)
|
||||
this._stats.encoded++
|
||||
this.emit('encode', { groupId: gid, parityCount })
|
||||
return { groupId: gid, parities }
|
||||
}
|
||||
|
||||
decodeGroup (groupId, received = {}) {
|
||||
const g = this._groups.get(String(groupId))
|
||||
if (!g) return null
|
||||
const have = (received.data || []).filter(Boolean).length + (received.parity || []).filter(Boolean).length
|
||||
if (have < g.dataCount) return null
|
||||
this._stats.recovered++
|
||||
this.emit('recover', { groupId })
|
||||
return { groupId, ok: true, dataShards: g.shards.length }
|
||||
}
|
||||
|
||||
recover (groupId) {
|
||||
return this.decodeGroup(groupId, { parity: [1] }) != null
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { groups: this._groups.size })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._groups.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PFecVideo, PROTOCOL }
|
||||
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-fec-video",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — fec video.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PFecVideo, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PFecVideo); t.is(PROTOCOL, 'fec-video/v1') })
|
||||
|
||||
test('encode group', async (t) => {
|
||||
const m = new HyperP2PFecVideo()
|
||||
const r = m.encodeGroup('g1', [Buffer.from('a'), Buffer.from('b')])
|
||||
t.ok(r.parities.length >= 1)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-helper-swarm-coordinator
|
||||
|
||||
**Protocol:** `helper-swarm-coordinator/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-helper-swarm-coordinator
|
||||
|
||||
**Protocol:** `helper-swarm-coordinator/v1`
|
||||
|
||||
**Export:** `{ HyperP2PHelperSwarmCoordinator, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PHelperSwarmCoordinator` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'helper-swarm-coordinator/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-helper-swarm-coordinator
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PHelperSwarmCoordinator } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PHelperSwarmCoordinator()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
|
||||
const { assertPeerId, assertStreamId, ROLES, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'helper-swarm-coordinator/v1'
|
||||
|
||||
class HyperP2PHelperSwarmCoordinator extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.topic = opts.topic || null
|
||||
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||
this.peerHex = b4a.toString(this.keyPair.publicKey, 'hex')
|
||||
this._helpers = new Map()
|
||||
this._assignments = new Map()
|
||||
this._stats = { assigned: 0, relayBytes: 0 }
|
||||
this.swarm = null
|
||||
this._peerMsgs = null
|
||||
}
|
||||
|
||||
registerHelper (peerId, capacityBps = 0) {
|
||||
const id = assertPeerId(peerId)
|
||||
const rec = { peerId: id, capacityBps: capacityBps | 0, usedBps: 0, role: ROLES.HELPER, at: Date.now() }
|
||||
this._helpers.set(id, rec)
|
||||
this.emit('helper', rec)
|
||||
return rec
|
||||
}
|
||||
|
||||
assignViewer (viewerId, helperIds = []) {
|
||||
const vid = assertPeerId(viewerId)
|
||||
const helpers = helperIds.map((h) => assertPeerId(h)).filter((h) => this._helpers.has(h))
|
||||
const assignment = { viewerId: vid, helpers, assignedAt: Date.now() }
|
||||
this._assignments.set(vid, assignment)
|
||||
this._stats.assigned++
|
||||
this._gossip({ type: 'assign', assignment })
|
||||
this.emit('assign', assignment)
|
||||
return assignment
|
||||
}
|
||||
|
||||
recordRelay (helperId, bytes) {
|
||||
const h = this._helpers.get(assertPeerId(helperId))
|
||||
if (!h) return false
|
||||
const n = Math.max(0, bytes | 0)
|
||||
h.usedBps += n
|
||||
this._stats.relayBytes += n
|
||||
this.emit('relay', { helperId, bytes: n })
|
||||
return true
|
||||
}
|
||||
|
||||
helpersForViewer (viewerId) {
|
||||
const a = this._assignments.get(assertPeerId(viewerId))
|
||||
return a ? [...a.helpers] : []
|
||||
}
|
||||
|
||||
availableHelpers () {
|
||||
return [...this._helpers.values()].filter((h) => h.usedBps < h.capacityBps)
|
||||
}
|
||||
|
||||
_gossip (payload) {
|
||||
if (this._peerMsgs) gossipSend(this, payload)
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, {
|
||||
helpers: this._helpers.size,
|
||||
viewers: this._assignments.size
|
||||
})
|
||||
}
|
||||
|
||||
async ready () {
|
||||
if (this.swarm || !this.topic) return this
|
||||
await initModuleSwarm(this, {
|
||||
keyPair: this.keyPair,
|
||||
topic: this.topic,
|
||||
protocol: PROTOCOL,
|
||||
onmessage: (d) => {
|
||||
if (d?.type === 'assign' && d.assignment) {
|
||||
this._assignments.set(d.assignment.viewerId, d.assignment)
|
||||
}
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||
this.swarm = null
|
||||
this._helpers.clear()
|
||||
this._assignments.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PHelperSwarmCoordinator, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-helper-swarm-coordinator",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — helper swarm coordinator.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PHelperSwarmCoordinator, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PHelperSwarmCoordinator); t.is(PROTOCOL, 'helper-swarm-coordinator/v1') })
|
||||
|
||||
test('assign helpers', async (t) => {
|
||||
const m = new HyperP2PHelperSwarmCoordinator()
|
||||
m.registerHelper('h1', 50_000_000)
|
||||
const a = m.assignViewer('viewer-1', ['h1'])
|
||||
t.is(a.helpers[0], 'h1')
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-latency-optimizer
|
||||
|
||||
**Protocol:** `latency-optimizer/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-latency-optimizer
|
||||
|
||||
**Protocol:** `latency-optimizer/v1`
|
||||
|
||||
**Export:** `{ HyperP2PLatencyOptimizer, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PLatencyOptimizer` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'latency-optimizer/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-latency-optimizer
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PLatencyOptimizer } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PLatencyOptimizer()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'latency-optimizer/v1'
|
||||
|
||||
class HyperP2PLatencyOptimizer extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.targetGlassMs = opts.targetGlassMs ?? 1500
|
||||
this._paths = new Map()
|
||||
this._stats = { measured: 0, optimized: 0 }
|
||||
}
|
||||
|
||||
measurePath (pathId, hops = []) {
|
||||
if (!pathId) throw new Error('pathId required')
|
||||
const rtt = hops.reduce((s, h) => s + (h.rttMs || 20), 0)
|
||||
const rec = { pathId: String(pathId), hops, rttMs: rtt, at: Date.now() }
|
||||
this._paths.set(rec.pathId, rec)
|
||||
this._stats.measured++
|
||||
return rec
|
||||
}
|
||||
|
||||
optimizeRoute (treeSnapshot = {}) {
|
||||
const nodes = treeSnapshot.nodes || []
|
||||
let bestRoot = treeSnapshot.root
|
||||
let bestRtt = Infinity
|
||||
for (const n of nodes) {
|
||||
const p = this._paths.get(n.peerId)
|
||||
const rtt = p ? p.rttMs : 50 * (n.childCount + 1)
|
||||
if (rtt < bestRtt) {
|
||||
bestRtt = rtt
|
||||
bestRoot = n.peerId
|
||||
}
|
||||
}
|
||||
this._stats.optimized++
|
||||
const plan = { root: bestRoot, estimatedGlassMs: bestRtt + 200, targetGlassMs: this.targetGlassMs }
|
||||
this.emit('optimize', plan)
|
||||
return plan
|
||||
}
|
||||
|
||||
suggestParent (candidates = [], tree = {}) {
|
||||
const plan = this.optimizeRoute(tree)
|
||||
if (candidates.includes(plan.root)) return plan.root
|
||||
return candidates[0] || plan.root
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { paths: this._paths.size })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._paths.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PLatencyOptimizer, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-latency-optimizer",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — latency optimizer.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PLatencyOptimizer, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PLatencyOptimizer); t.is(PROTOCOL, 'latency-optimizer/v1') })
|
||||
|
||||
test('optimize', async (t) => {
|
||||
const m = new HyperP2PLatencyOptimizer()
|
||||
m.measurePath('p1', [{ rttMs: 10 }, { rttMs: 15 }])
|
||||
const plan = m.optimizeRoute({ root: 'a', nodes: [{ peerId: 'a', childCount: 1 }] })
|
||||
t.ok(plan.estimatedGlassMs > 0)
|
||||
await m.close()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.0-scaffold
|
||||
|
||||
- Initial media-streaming category implementation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# hyper-p2p-live-edge-manager
|
||||
|
||||
**Protocol:** `live-edge-manager/v1`
|
||||
|
||||
Media-streaming module for peer-assisted live/VOD delivery. See [`docs/api.md`](docs/api.md).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: hyper-p2p-live-edge-manager
|
||||
|
||||
**Protocol:** `live-edge-manager/v1`
|
||||
|
||||
**Export:** `{ HyperP2PLiveEdgeManager, PROTOCOL }`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PLiveEdgeManager` — see [`index.js`](../index.js) for methods, events, and `getStats()`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- `async ready()` — optional Hyperswarm join when `topic` is set
|
||||
- `async close()` — teardown
|
||||
- `getStats()` — metrics + `protocol: 'live-edge-manager/v1'`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: hyper-p2p-live-edge-manager
|
||||
|
||||
Part of the **media-streaming** category. Composes with network-stack, core-infrastructure, scheduling, measurement, and trust modules per `MODULE_REGISTRY.yaml`.
|
||||
@@ -0,0 +1,14 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PLiveEdgeManager } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PLiveEdgeManager()
|
||||
await m.ready()
|
||||
console.log(m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const { assertStreamId, mediaStats } = require('../../_shared/media-streaming-base.js')
|
||||
|
||||
const PROTOCOL = 'live-edge-manager/v1'
|
||||
|
||||
class HyperP2PLiveEdgeManager extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.maxLagMs = opts.maxLagMs ?? 3000
|
||||
this._edges = new Map()
|
||||
this._stats = { updates: 0, stale: 0 }
|
||||
}
|
||||
|
||||
setLiveEdge (streamId, seq, wallAt = Date.now()) {
|
||||
const sid = assertStreamId(streamId)
|
||||
const edge = { streamId: sid, seq: seq | 0, wallAt, updatedAt: Date.now() }
|
||||
this._edges.set(sid, edge)
|
||||
this._stats.updates++
|
||||
this.emit('edge', edge)
|
||||
return edge
|
||||
}
|
||||
|
||||
getLiveEdge (streamId) {
|
||||
return this._edges.get(assertStreamId(streamId)) || null
|
||||
}
|
||||
|
||||
edgeLagMs (streamId, now = Date.now()) {
|
||||
const e = this.getLiveEdge(streamId)
|
||||
if (!e) return null
|
||||
return Math.max(0, now - e.wallAt)
|
||||
}
|
||||
|
||||
isFresh (streamId, chunkWallAt, now = Date.now()) {
|
||||
const lag = this.edgeLagMs(streamId, now)
|
||||
if (lag == null) return false
|
||||
const behind = now - chunkWallAt
|
||||
return behind <= this.maxLagMs + lag
|
||||
}
|
||||
|
||||
listStaleStreams (now = Date.now()) {
|
||||
const out = []
|
||||
for (const e of this._edges.values()) {
|
||||
if (now - e.updatedAt > this.maxLagMs) {
|
||||
out.push(e.streamId)
|
||||
this._stats.stale++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return mediaStats(this._stats, PROTOCOL, { streams: this._edges.size })
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._edges.clear()
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PLiveEdgeManager, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-live-edge-manager",
|
||||
"version": "0.0.0-scaffold",
|
||||
"description": "P2P media streaming — live edge manager.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PLiveEdgeManager, PROTOCOL } = require('../index.js')
|
||||
|
||||
test('exports', (t) => { t.ok(HyperP2PLiveEdgeManager); t.is(PROTOCOL, 'live-edge-manager/v1') })
|
||||
|
||||
test('live edge', async (t) => {
|
||||
const m = new HyperP2PLiveEdgeManager()
|
||||
m.setLiveEdge('live', 100, Date.now())
|
||||
t.is(m.getLiveEdge('live').seq, 100)
|
||||
await m.close()
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user