LogoPear Docs
How ToStream and share media

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.

Start from the desktop boilerplate. This guide has a frontend, so it builds on hello-pear-electron—the renderer, preload bridge, and Bare worker it extends. Read Start from the hello-pear-electron template first. The Pear-end logic itself is portable—only the UI is desktop-specific (see Runtime and languages); the terminal template hello-pear-bare has no UI layer. See all starting points in Start from a template.

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

LayerChange
DependenciesAdd hyperblobs, hypercore-blob-server, hypercore-id-encoding, and get-mime-type.
WorkerAdd a Hyperblobs core in the worker; store each video as one blob and attach a blob-server link to every entry.
Worker transportAdd an add-video ingest (an { type: 'add-video', filePath } control message) and a videos snapshot whose entries carry a playable link.
RendererRender 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-type

Publish 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):

workers/video-room.js
  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):

workers/video-room.js
  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 host

Drop 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

On this page