Stream stored video in a peer-to-peer app
Serve stored video files over hypercore-blob-server so peers can stream them with range requests on top of the hello-pear-electron scaffold.
This guide shows you how to stream a stored video file peer-to-peer by adapting the hello-pear-electron scaffold to publish video blobs through Hyperblobs and serve them with hypercore-blob-server. The reference implementation is pear-video-stream.
This guide is about the Pear-end, not the shell. The code below lives in the Bare worker — the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across desktop (Electron), mobile (React Native via Bare iOS / Bare Android), and terminal. The example apps ship an Electron shell, but only the UI half changes per platform — the logic here stays the same. See Runtime and languages for the cross-platform model and current support.
This is a delta-only how-to. The shared scaffold is explained in the Start from the hello-pear-electron template tutorial — read it first.
- Stream a live camera in a peer-to-peer app — the live counterpart that builds on the same blob plumbing.
Before you begin
- A working clone of
hello-pear-electron(or your own app built from the getting-started path). - A stored video file (
.mp4,.webm) you want to share.
What changes
| Layer | Change |
|---|---|
| Dependencies | Add hyperblobs, hypercore-blob-server, hypercore-id-encoding, and get-mime-type. |
| Worker | Add a Hyperblobs core in the worker; store each video as one blob and attach a blob-server link to every entry. |
| Worker transport | Add an add-video ingest (an { type: 'add-video', filePath } control message) and a videos snapshot whose entries carry a playable link. |
| Renderer | Render a video player on the selected entry's link. |
The Electron shell, preload bridge, packaging, and graceful teardown stay as in hello-pear-electron.
Steps
Add the dependencies
npm install hyperblobs hypercore-blob-server hypercore-id-encoding get-mime-typePublish each video as a Hyperblob
workers/video-room.js (VideoRoom) keeps the tutorial's Autobase + pairing, and constructs a Hyperblobs core plus a hypercore-blob-server. addVideo resolves the file name and checks the MIME type with get-mime-type, rejecting anything that is not a video/* type (L180–L184). It then pipes the file off disk into a blobs write stream (L186–L192), captures the resulting blob descriptor (the blobs core key plus the write stream's byte id, L193), and appends { id, name, type, blob, info } to the base so the view records it (L196–L198):
async addVideo (filePath, info) {
const name = path.basename(filePath)
const type = getMimeType(name)
if (!type || !type.startsWith('video/')) {
throw new Error('Only video files are allowed')
}
const rs = fs.createReadStream(filePath)
const ws = this.blobs.createWriteStream()
await new Promise((resolve, reject) => {
ws.on('error', reject)
ws.on('close', resolve)
rs.pipe(ws)
})
const blob = { key: idEnc.normalize(this.blobs.core.key), ...ws.id }
const id = Math.random().toString(16).slice(2)
await this.base.append(
VideoDispatch.encode('@pear-video-stream/add-video', { id, name, type, blob, info })
)
}Serve the blob locally
The server speaks HTTP range requests, so the player handles seeking without downloading the whole file. getVideos reads the stored entries from the view (L164), and for each one whose blobs core it has not seen yet, opens that core and joins its swarm topic on demand (L165–L172). It then maps every entry to attach a playable info.link from blobServer.getLink before returning to the renderer (L173–L176):
async getVideos ({ reverse = true, limit = 100 } = {}) {
const videos = await this.view.find('@pear-video-stream/videos', { reverse, limit }).toArray()
for (const item of videos) {
if (!this.blobsCores[item.blob.key]) {
const blobsCore = this.store.get({ key: idEnc.decode(item.blob.key) })
this.blobsCores[item.blob.key] = blobsCore
await blobsCore.ready()
this.swarm.join(blobsCore.discoveryKey)
}
}
return videos.map(item => {
const link = this.blobServer.getLink(item.blob.key, { blob: item.blob, type: item.type })
return { ...item, info: { ...item.info, link } }
})
}Render a <video> element
In the renderer, create a <video controls> element and set its src to video.info.link for the selected entry. The first peer who clicks Play triggers a range request, the worker pulls the byte range from the Hyperblob, and the player streams.
Run it
npm run build
# host
npm start -- --storage /tmp/video-host --name hostDrop a video file into the window (the renderer forwards the path via webUtils.getPathForFile). In a second terminal:
npm start -- --storage /tmp/video-viewer --name viewer --invite <invite>The viewer sees the entry, clicks Play, and the player streams range by range over the replicated Hyperblobs.
Where to go next
- Store and serve large media with Hyperblobs — the Pear/Bare blob primitive behind this app, with no UI.
- Stream a live camera in a peer-to-peer app — same blob mechanics for live frames.
- Back up photos in a peer-to-peer app — same scaffold with
bare-ffmpeg/bare-mediafor media decoding. - Storage and distribution — why range-served blobs scale well in a P2P swarm.