logicsrc/docs/openswarm/ipvideo.md
Anthony Ettinger 4fed2681ec
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run
Add the LogicSRC OpenSwarm specification family (#142)
BitTorrent distributes bytes better than any CDN and has never been a
product: nobody is paid to seed, and nothing in it is private. Every
Profullstack media property answers that with a central HTTP proxy and a
pass system bolted on, and pays for every byte it serves.

OpenSwarm is an add-on to BitTorrent, carried as BEP 10 extension
messages, that fixes both. The swarm carries AES-256-CTR ciphertext whose
integrity is pinned by two SHA-256 merkle roots in a signed manifest, so
a tracker or DHT node learns an infohash and a size. A leecher buys a
pass over x402 in USDC (the same exchange x402-gateway runs for crawl
passes, settled by CoinPay), a seeder serves inside a bounded credit
window, and the leecher signs a cumulative voucher for every verified
batch. Whoever seeds gets paid. Vanilla clients remain valid members and
browsers remain first-class peers.

Adding a file mints a key pair for it, derived from one publisher seed by
default so there is one thing to back up: the public half is the file's
identity and its BEP 46 key, the private half signs the manifest and
authorises grants and payout changes, and a separate content key
encrypts the bytes and is sealed to paying peers.

The family: a core (records, keys, hashing, transports, discovery,
events), ipfile, ippay, ipdb (a signed hash-chained catalogue with heads
on the DHT), ipaudio, ipvideo, iplive (paid relays with backpressure)
and ipname (Moshpit pins and DNS TXT). Plus the c0mpute.com integration
with seven workload types and thirteen use cases, a proposed ip CLI,
conformance profiles, a security model and an FAQ.

Registered on the site the way OpenCreds is: nav entry, docs registry,
sitemap, a /openswarm landing page, PRD 0005, and the MCP prd_next_id
expectation moved to 0006.

Specs only. No code, no schemas, no reference implementation.


Claude-Session: https://claude.ai/code/session_01YafYxayh7Gqe5MWNNQMev2

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 15:05:57 -07:00

271 lines
11 KiB
Markdown

# ipvideo: Video on Demand on ipfile Swarms
Version: **0.1** (draft)
Status: draft. Extends the OpenSwarm core ([spec.md](./spec.md)) and rides
on [ipfile](./ipfile.md), [ippay](./ippay.md) and [ipdb](./ipdb.md).
Nothing below is implemented.
## 1. Scope
`ipvideo` defines the records that make a set of `ipfile` swarms playable
as a film, an episode or a clip:
1. A **title**: renditions (CMAF fragmented MP4, one `ipfile` each), a
segment index, subtitles, thumbnails, chapters, artwork.
2. **Playback rules**: segment-aligned adaptive bitrate over pieces, MSE in
the browser.
3. A **gateway bridge** that renders HLS for players that speak nothing
else.
4. **Transcode on c0mpute**: how the renditions get made.
Bytes, keys, payment and discovery are `ipfile` and `ippay`. No new wire
messages.
### Non-goals
Live (that is `iplive`). Interactive video. Ad insertion (c0mpute's DAI
DIPs may reference a title; this document does not). DRM in the device
sense.
## 2. Terminology
| Term | Meaning |
| --- | --- |
| **rendition** | One encoding ladder step, stored as one `ipfile` in CMAF form. |
| **segment** | A fragmented MP4 `moof + mdat` pair, starting with a keyframe. |
| **init segment** | The `ftyp + moov` bytes a decoder needs before any segment. |
| **segment index** | The list of `{ t, d, o, l }` for every segment of a rendition. |
| **ladder** | The set of renditions of a title, aligned on segment boundaries. |
## 3. Data model
### 3.1 Title
```json
{
"openswarm": "0.1",
"type": "ipvideo.title",
"publisher": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
"title": "The Interview",
"kind": "movie",
"series": null,
"season": null,
"episode": null,
"year": 2026,
"durationMs": 5400000,
"language": "en",
"artwork": {
"poster": "ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94",
"backdrop": null
},
"renditions": [
{
"id": "1080p",
"container": "cmaf",
"codecs": "avc1.640028,mp4a.40.2",
"width": 1920,
"height": 1080,
"frameRate": 24,
"bitrateKbps": 5000,
"file": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
"size": 3375000000,
"init": { "o": 0, "l": 1524 },
"segmentsFile": "ed25519:00bc9c01ca00938016e276fe5b5748cfe13fd02bc53cc651535682c8dd5c83be"
},
{
"id": "480p",
"container": "cmaf",
"codecs": "avc1.64001e,mp4a.40.2",
"width": 854,
"height": 480,
"frameRate": 24,
"bitrateKbps": 1400,
"file": "ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94",
"size": 945000000,
"init": { "o": 0, "l": 1498 },
"segments": [
{ "t": 0, "d": 4000, "o": 1498, "l": 702113 },
{ "t": 4000, "d": 4000, "o": 703611, "l": 698402 }
]
}
],
"audio": [
{ "id": "en", "language": "en", "label": "English", "channels": 2, "default": true }
],
"subtitles": [
{ "id": "en-cc", "language": "en", "kind": "captions", "format": "vtt", "file": "ed25519:c9f17d4eaf2b28122b46d111cef6697d2c3f708a19d4628bce5c0304481b355b" }
],
"thumbnails": {
"file": "ed25519:4b74eb43677e4d03af5fb0856333f9aa21d9a5a3bbf944b13aa7eef379c7a342",
"vtt": "ed25519:ba8cd1a97b865a05cdc5840c63b086dd463624a831874c1ba0d7c4ce6f806f7a",
"intervalMs": 10000
},
"chapters": [
{ "t": 0, "title": "Opening" },
{ "t": 1830000, "title": "The question" }
],
"royalties": [],
"createdAt": "2026-09-05T18:20:00.000Z",
"sigs": [
{
"alg": "ed25519",
"key": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
"sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
}
]
}
```
(The `480p` segment list is truncated to two entries; a real one has one
entry per segment.)
| Field | Rule |
| --- | --- |
| `kind` | `movie`, `episode`, `clip`, `trailer`. `series`, `season`, `episode` are set for `episode`. |
| `renditions[].container` | `cmaf` in this version: fragmented MP4 per ISO 23000-19, one segment per `moof`. |
| `renditions[].codecs` | RFC 6381 string as MSE `isTypeSupported` wants it. |
| `renditions[].file` | The `ipfile` manifest's file key. `size` MUST match. |
| `renditions[].init` | Byte range of the init segment within the plaintext. |
| `renditions[].segments` | Inline index, or `segmentsFile`: file key of a free `ipfile` (`keys.public` set) whose plaintext is the JSON array. Inline when under 64 KiB, file otherwise. |
| `segments[].t`, `d` | Presentation start and duration in ms. `o`, `l`: byte offset and length in the plaintext. |
| `audio` | Audio tracks muxed in the renditions. Alternate-language audio is a rendition with `codecs` of audio only and `video: false`. |
| `subtitles[].format` | `vtt` in this version. Each is a free `ipfile` unless the publisher prices it. |
| `thumbnails` | A sprite sheet `ipfile` and a WebVTT file mapping times to sprite regions, as hls.js and video.js expect. |
| `royalties` | As `ipaudio` §5. Empty means the whole publisher share goes to `payTo`. |
Every rendition MUST share the same `t` list. Every segment MUST start with
a keyframe. Segment duration SHOULD be 4000 ms, matching c0mpute's
live-stream DIP (`-hls_time 4`) so a recorded live stream and a VOD title
use one ladder.
### 3.2 Where records live
`ipdb` under `title:<id>` in the publisher's feed. Renditions, subtitles
and thumbnails are `ipfile` manifests in the same feed with
`meta.title = <title record id>`.
## 4. Playback
### 4.1 Segment to pieces
To play segment `s` of rendition `r`:
```
first piece = floor(s.o / pieceLength)
last piece = floor((s.o + s.l - 1) / pieceLength)
```
Request those pieces; decrypt; verify against the plaintext piece layer;
slice `[s.o, s.o + s.l)`; append to the MSE `SourceBuffer` after the init
segment. With 1 MiB pieces and a 5 Mbps rendition a 4 s segment is about
2.5 MiB, so two to three pieces.
### 4.2 Adaptive bitrate
Switch at segment boundaries only. The next segment is fetched from the
rendition whose `bitrateKbps` is the highest below 0.8 times the measured
piece throughput over the last 10 seconds, with a two-segment hysteresis
before stepping up. Because `t` lists are shared, the MSE buffer stays
contiguous across a switch; a new init segment is appended before the
first segment of the new rendition, as in any CMAF player.
### 4.3 Buffering and priority
Hold 30 seconds ahead. Request the pieces for the next segment with
sequential priority (what media-streamer's `torrent.select` does for range
streaming today). Seeking to time `t`: find the segment with the largest
`t <= target`, drop outstanding requests, start there. A player MUST keep
sending vouchers for bytes received while seeking, including bytes it then
discards; discarded bytes were still served.
### 4.4 Browser
A browser peer is a WebTorrent client with the `ipfile` extension, a
WebCrypto AES-CTR decryptor, and MSE. This is bittorrented.com's player
with three additions and no server in the path. Where MSE is unavailable
(some iOS contexts), the gateway bridge in §5 applies.
## 5. Gateway bridge
A keeper gateway renders HLS from the title record:
```
GET /video/<title id>/master.m3u8
GET /video/<title id>/<rendition id>/index.m3u8
GET /video/<title id>/<rendition id>/data (plaintext, Range)
Authorization: Bearer <base64url pass>
```
The media playlist uses `#EXT-X-MAP` for the init range and
`#EXT-X-BYTERANGE` for every segment against the single `data` URL, so the
gateway serves plaintext by range from decrypted pieces and never has to
split files. Subtitles become `#EXT-X-MEDIA` entries pointing at
`/file/<file key>/data`. Credit and voucher headers are as `ippay` §7; a
player that cannot send vouchers (a bare `<video>` tag) is limited to the
pass's first credit window unless the gateway is configured to trust the
pass for the title's full size.
HLS's own encryption (`EXT-X-KEY` with AES-128-CBC) is not used. The
gateway is a keeper; it decrypts. A publisher who does not want any
gateway holding a key does not delegate to one, and browser MSE is the
only path.
## 6. Transcode on c0mpute
`ip video publish <source> --ladder default` submits one c0mpute job per
rendition. The existing `ffmpeg.transcode` workload takes
`TranscodeSpec { codec, bitrate_bps, width, height, keyframe_interval,
hardware_pref, extra_ffmpeg_args }`; `ipvideo` adds `extra_ffmpeg_args`
that force CMAF output and a 4 s keyframe interval, and expects the result
to include the segment index. The proposed `ipvideo.transcode` workload
(`c0mpute.md` §3) is that: a wrapper whose `TranscodeResult` carries
`segments`, `init`, `codecs` and gapless-irrelevant fields, and whose
output is handed to `ip file add` on the worker so the rendition is seeded
from where it was made.
Default ladder:
| id | codecs | size | bitrate |
| --- | --- | --- | --- |
| `1080p` | `avc1.640028,mp4a.40.2` | 1920x1080 | 5000 kbps |
| `720p` | `avc1.64001f,mp4a.40.2` | 1280x720 | 2800 kbps |
| `480p` | `avc1.64001e,mp4a.40.2` | 854x480 | 1400 kbps |
| `360p` | `avc1.64001e,mp4a.40.2` | 640x360 | 800 kbps |
| `1080p-av1` | `av01.0.08M.08,opus` | 1920x1080 | 3000 kbps, optional |
## 7. Security and privacy
As `ipfile`. A segment index reveals segment sizes, which for a
constant-quality encode leak scene complexity over time; a publisher who
cares uses constant bitrate. Subtitle files are small free swarms and
reveal the dialogue to anyone; price them if that matters.
## 8. Events
| Type | Emitted by |
| --- | --- |
| `com.logicsrc.openswarm.ipvideo.title.published.v1` | publisher |
| `com.logicsrc.openswarm.ipvideo.transcode.completed.v1` | c0mpute worker |
## 9. Implementations
| Piece | Exists | Where | What is new |
| --- | --- | --- | --- |
| Browser WebTorrent player, MSE-compatible fMP4 output, HLS fallback, range seeking | Yes | `media-streamer` (`media-player-modal.tsx`, `transcoding.ts`, `/api/stream/hls`) | Piece decryption before MSE; the title record instead of `vod_titles` |
| Hardware ffmpeg transcode job with codec ladder | Yes | `c0mpute` (`c0mpute-transcode`, `TranscodeSpec`) | CMAF output flags and the segment index in the result |
| VOD catalogue with per-title price and pass window | Yes (central) | `media-streamer` `vod_titles`, `vod_providers` | The title record and `ipfile` pricing replace both tables |
| Thumbnail VTT, chapters, subtitle tracks | Partly | hls.js and video.js conventions | The record fields |
| Title record, segment index file, HLS bridge from records | No | | All of it |
## 10. Conformance
A player fetches by segment per §4.1, switches at boundaries per §4.2,
and vouchers for discarded bytes per §4.3. A publisher emits aligned
ladders with keyframe-aligned segments. A gateway implementing §5 renders
byte-range HLS and applies `ippay` §7.
## 11. Version history
| Version | Date | Change |
| --- | --- | --- |
| 0.1 | 2026-09-05 | Initial draft. |