@walrus-streamkit/sdk
v0.1.4
Published
Backend SDK for video infrastructure on Walrus (blob storage) and Sui (ownership, payments, access) — upload, encrypt, publish, and resolve playback for VOD and pseudo-live. Runs on Node 18+ and Bun.
Downloads
646
Readme
@walrus-streamkit/sdk
Backend SDK for developer-first video infrastructure on Walrus (blob storage — the data plane) and Sui (ownership, payments, access — the control plane). Embed it in your own Node/Bun server to upload, encrypt, publish, and resolve playback for VOD and pseudo-live video.
This is the backend SDK: your server holds the signer and pays for storage. For viewer-side playback where the end-user's wallet pays for access and unlocks encrypted content, use @walrus-streamkit/client.
⚠️ Server-side only. Uses
ffmpegandnode:built-ins (fs,child_process,crypto) — it does not run in a browser. Runs on Node 18+ and Bun.
Contents
- Install
- Quickstart
- Write modes
- Configuration
- Guides — upload, encryption, playback, live, metrics, publish
- API reference
- Caveats
Install
# Bun
bun add @walrus-streamkit/sdk
# Node — pick your package manager
npm install @walrus-streamkit/sdk
pnpm add @walrus-streamkit/sdk
yarn add @walrus-streamkit/sdkThe package ships both ESM and CommonJS builds with bundled type declarations, so it imports cleanly on Node and Bun:
import { WalrusStreamKit } from "@walrus-streamkit/sdk"; // ESM (Node ESM, Bun, bundlers)const { WalrusStreamKit } = require("@walrus-streamkit/sdk"); // CommonJS (Node require)Runtime requirements
- Node 18+ or Bun 1.1+ — the SDK is runtime-agnostic. Every path (
uploadFile(),upload(),getPlayback(), andupload-relayquilt encoding via WASM) runs on both; it relies only onnode:built-ins (fs,child_process,crypto) that both runtimes provide. Use whichever your backend already runs. - ffmpeg on
PATH— transcodes source media into HLS segments (uploadFile()and any transcoding pipeline). Install with your OS package manager (brew install ffmpeg,apt-get install ffmpeg,choco install ffmpeg), or pointFFMPEG_PATHat a custom binary. This is a system dependency, not an npm/Bun one. - A funded Sui testnet/mainnet key when using the default
upload-relaywrite mode — storing on Walrus costs gas + a storage tip.
Quickstart
Offline — no keys, no network (memory mode)
import { WalrusStreamKit } from "@walrus-streamkit/sdk";
const kit = new WalrusStreamKit({
writeMode: "memory",
gatewayBase: "http://localhost:3000",
relayerUrl: "http://localhost:3000",
});
// `data` is one or more already-segmented .ts buffers (upload() does not transcode):
// one buffer → one segment. For a real file, use uploadFile() (transcodes into many).
const { videoId, segments } = await kit.upload({ data: tsBytes });
console.log(videoId, segments.length); // e.g. "up-1a2b3c4d", 1memory keeps everything in RAM — ideal for tests and local dev. Nothing is durable.
Testnet — real Walrus (upload-relay mode)
import { WalrusStreamKit } from "@walrus-streamkit/sdk";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
const kit = new WalrusStreamKit({
writeMode: "upload-relay", // the default
network: "testnet",
signer: Ed25519Keypair.fromSecretKey(process.env.SUI_KEY!), // your platform key — it pays
relayerUrl: "http://localhost:8787", // your relayer
});
const res = await kit.uploadFile({ path: "./talk.mp4" }); // transcode → store on Walrus
const play = await kit.getPlayback(res.videoId); // { kind: "public", manifestUrl }
// → feed play.manifestUrl to hls.jsWrite modes
| Mode | Durable | Needs signer | Who pays | Use for |
| --- | --- | --- | --- | --- |
| memory | ❌ (RAM only) | No | — | tests, local dev, offline |
| publisher | ✅ | No | the Walrus publisher (custodial) | quick custodial writes |
| upload-relay (default) | ✅ | Yes | you — gas + a Walrus tip | production, non-custodial |
upload-relay quilt-encodes the bytes locally, your signer signs the Walrus registerBlob (bundling a tip), and the bytes ship to Walrus through your relayer's upload-proxy endpoint. Supported on testnet/mainnet only.
Configuration
new WalrusStreamKit(config?: StreamKitConfig)Your config is shallow-merged over the testnet defaults (only keys you set override).
| Key | Type | Default (testnet) |
| --- | --- | --- |
| writeMode | "upload-relay" \| "publisher" \| "memory" | "upload-relay" |
| signer | @mysten/sui Signer | — (required for upload-relay + publish) |
| network | "testnet" \| "mainnet" \| "devnet" \| "localnet" | "testnet" |
| packageId | string | 0xda93702b…f3e5 (deployed walrus_video v2) |
| channelId | string | — (required for publish()) |
| relayerUrl | string | http://localhost:8787 |
| walrusPublisher | string | https://publisher.walrus-testnet.walrus.space |
| walrusAggregator | string | https://aggregator.walrus-testnet.walrus.space |
| walrusUploadRelay | string | https://upload-relay.testnet.walrus.space |
| gatewayBase | string | http://localhost:8787 |
| fetchImpl | typeof fetch | globalThis.fetch |
⚠️ Upload-relay host invariant: your
walrusUploadRelayand the relayer's ownWALRUS_UPLOAD_RELAYmust point at the same host (testnet:upload-relay.testnet.walrus.space). Otherwise the tip pays the wrong operator and the relay rejects the upload.
Guides
Upload a file (recommended)
const res = await kit.uploadFile({ path: "./video.mp4", visibility: 0 });
// res: { videoId, manifestBlobId, integrityRoot, segments, sealKeyB64? }uploadFile transcodes the input with ffmpeg, then streams the resulting segments into storage one at a time — bounded memory, so it handles multi-hour videos. Runs on Node and Bun (segments are read via node:fs).
Upload pre-segmented bytes
const res = await kit.upload({ data: [seg0, seg1] }); // each item is a complete .ts segmentupload() does not transcode — data is treated as already-segmented .ts parts. The whole payload sits in RAM, so use it for small clips or live segments, not large files.
Visibility & encryption
visibility selects the on-chain access kind:
| value | name | encrypted (Seal)? |
| --- | --- | --- |
| 0 | public | no |
| 1 | allowlist | yes |
| 2 | paid | yes |
| 3 | subscription | yes |
For any visibility !== 0, each segment is AES-128 encrypted and the content key is Seal-wrapped; the wrapped key is returned as res.sealKeyB64. Register it with your relayer so it can serve the key — the viewer's wallet then unlocks it on-chain via seal_approve_*:
const res = await kit.uploadFile({ path: "./paid.mp4", visibility: 2 });
await fetch(`${relayerUrl}/v1/videos/${res.videoId}/register`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
manifestBlobId: res.manifestBlobId,
segments: res.segments,
visibility: 2,
encrypted: true,
sealKeyB64: res.sealKeyB64, // ← required for encrypted videos
}),
});Resolve playback
const pb = await kit.getPlayback(videoId, viewerAddress /* optional */);
// { kind: "public", manifestUrl }
// { kind: "encrypted", manifestUrl, keyUrl, sealId, isOwner }Feed manifestUrl to hls.js. For kind: "encrypted", the actual Seal decrypt + playback happens in the viewer client (@walrus-streamkit/client) — the user's wallet signs the unlock.
Pseudo-live → durable VOD
const live = kit.startLive({ segDurationSec: 2, windowSize: 6 });
await live.pushSegment(tsBytes); // push .ts chunks as they're captured
const playlist = live.mediaPlaylist(); // current sliding-window HLS playlist (no #EXT-X-ENDLIST)
const vod = await live.end(); // finalize → full VOD manifest, reusing the same blobsUse writeMode: "publisher" or "memory" for live — per-segment upload-relay writes are too slow for a live window.
Relayer metrics
const m = await kit.getMetrics();
// { cache: { hits, misses, sets, hitRatio, bytes? },
// uploadQueue: { waiting, active, completed, failed, delayed },
// requests: { byPath, total }, uptimeMs }Reads operational metrics from your relayer's GET /v1/metrics.
Publish on-chain (optional)
const { videoAssetId, txDigest } = await kit.publish({
videoId,
manifestBlobId: res.manifestBlobId!,
integrityRoot: res.integrityRoot!,
durationMs: 600_000,
channelId: "0x…", // or set config.channelId
visibility: 0,
});Signs video::create / video::complete, minting a VideoAsset owned by your signer. Requires signer, a channelId, and network testnet/mainnet.
API reference
import { … } from "@walrus-streamkit/sdk"
| Export | Signature | Status |
| --- | --- | --- |
| WalrusStreamKit | new (config?: StreamKitConfig) | ✅ |
| .upload | (input: UploadInput) => Promise<UploadResult> | ✅ |
| .uploadFile | (input: UploadFileInput) => Promise<UploadResult> | ✅ |
| .getPlayback | (videoId: string, viewer?: string) => Promise<PlaybackResult> | ✅ |
| .publish | (input: PublishInput) => Promise<PublishResult> | ✅ |
| .startLive | (opts?: { segDurationSec?; windowSize?; videoId? }) => LiveSession | ✅ |
| .getMetrics | () => Promise<RelayerMetrics> | ✅ |
| .getStatus | (videoId: string) => Promise<ProcessingStatus> | 🚧 not implemented |
| .createPolicy | (input: PolicyInput) => Promise<PolicyResult> | 🚧 not implemented |
| .addToAllowlist | (policyId: string, address: string) => Promise<{ digest }> | 🚧 not implemented |
| .webhooks.register / .webhooks.list | webhook management | 🚧 not implemented |
| LiveSession | pushSegment(ts) / mediaPlaylist() / end() | ✅ |
| processUpload | low-level pipeline → ProcessedUpload | ✅ |
| MemoryStorageAdapter, FfmpegTranscoder, createSealWrapper | advanced building blocks | ✅ |
| resolveConfig, testnetDefaults, NotImplementedError | config + helpers | ✅ |
🚧 Methods marked not implemented throw NotImplementedError until their milestone lands.
Key types
UploadInput { data: Uint8Array | Uint8Array[]; visibility?: 0|1|2|3; policyId?: string; durationMsHint?: number }
UploadFileInput { path: string; visibility?: number }
UploadResult { videoId: string; jobId?: string; manifestBlobId?: string; integrityRoot?: string; segments: string[]; sealKeyB64?: string }
PlaybackResult { kind: "public"; manifestUrl: string }
| { kind: "encrypted"; manifestUrl: string; keyUrl: string; sealId: string; isOwner: boolean }
PublishInput { videoId; manifestBlobId; integrityRoot; durationMs; channelId?; visibility?; policyId? }Caveats
- Server-side only — ffmpeg +
node:built-ins; not browser-compatible. Viewer playback of encrypted video lives in@walrus-streamkit/client. - Runtime: Node 18+ or Bun. Every path —
uploadFile(),upload(),getPlayback(), andupload-relayquilt encoding (WASM) — runs on both.ffmpegmust be onPATHfor transcoding. upload-relayrequires asignerandnetworktestnet/mainnet, and its host must match the relayer'sWALRUS_UPLOAD_RELAY.sealKeyB64is returned only forvisibility !== 0; you must register it with the relayer or encrypted playback can't serve the key.- Live over
upload-relayis impractical — usepublisherormemory. memorymode is not durable (RAM only; lost on restart).- 🚧
getStatus,createPolicy,addToAllowlist, andwebhooksare not implemented yet.
See also
examples/sdk-demo— a runnable Bun server wiring this SDK to a relayer (public + encrypted upload, playback, live, metrics).@walrus-streamkit/client— frontend SDK: viewer wallet pays + Seal-unlocks encrypted playback.apps/relayer— upload proxy, cached read origin, and Seal key broker.docs/system-design.md— full architecture.
