LogoPear Docs
How ToStream and share media

Back up photos in a peer-to-peer app

Decode local photos with bare-ffmpeg/bare-media and push them into a Hyperblobs store on top of the hello-pear-electron scaffold.

This guide shows you how to back up photos peer-to-peer by adapting the hello-pear-electron scaffold to decode local images with bare-ffmpeg/bare-media and push them into a Hyperblobs store. The reference implementation is pear-photo-backup.

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.

Before you begin

  • A working clone of hello-pear-electron (or your own app built from the getting-started path).
  • Comfort with Hyperblobs and the Bare native module set.

What changes

LayerChange
DependenciesAdd bare-ffmpeg, bare-media, get-mime-type, hyperblobs, hypercore-blob-server, hypercore-id-encoding.
WorkerStore the full file as one Hyperblob and generate a small inline preview (a data: URL) — bare-media for images, bare-ffmpeg for video — recorded alongside the blob id in the view.
Worker transportUse a JSON-over-pipe control surface: the renderer sends { type: 'add-video', path } and the worker emits { type: 'videos', videos } events whose entries carry a blob-server link plus the inline preview.
RendererShow a grid that renders each entry's inline preview and opens the full blob via its link.

Steps

Add the dependencies

npm install bare-ffmpeg bare-media get-mime-type hyperblobs hypercore-blob-server hypercore-id-encoding

bare-ffmpeg and bare-media are Bare native modules — they ship prebuilt binaries via bare-sidecar and only work inside the Bare worker, never in Electron's main or renderer.

Store the file and generate a preview inside the worker

workers/video-room.js (VideoRoom, shared with the video-stream how-to but extended for images) defines addVideo (L181), which checks the MIME type with get-mime-type and rejects anything that is not an image or video (L183–L186). It then streams the full file bytes into a Hyperblobs write stream (L188–L194) and captures the resulting blob id (L195). Only then does it generate a small inline preview — bare-media for images, bare-ffmpeg for video (L197) — and append the record to the base (L200–L202):

workers/video-room.js
  async addVideo (filePath, info) {
    const name = path.basename(filePath)
    const type = getMimeType(name)
    if (!(type.startsWith('image/') || type.startsWith('video/'))) {
      throw new Error('Only image/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 preview = type.startsWith('image/') ? await createPreviewImage(filePath) : await createPreviewVideo(filePath)

    const id = Math.random().toString(16).slice(2)
    await this.base.append(
      VideoDispatch.encode('@pear-photo-backup/add-video', { id, name, type, blob, info: { ...info, preview } })
    )
  }

The preview is a base64 data: URL kept inline in the record's info, so the grid can paint immediately without fetching the full blob. Images are resized with bare-media: createPreviewImage decodes the file, resizes it to a 256×256 bound, and re-encodes it as WebP (L6–L9), then returns the bytes as a base64 data: URL (L10):

workers/create-preview-image.js
const { image } = require('bare-media')

const MIMETYPE = 'image/webp'

async function createPreviewImage (filePath) {
  const buffer = await image(filePath)
    .decode()
    .resize({ maxWidth: 256, maxHeight: 256 })
    .encode({ mimetype: MIMETYPE })
  return `data:${MIMETYPE};base64,${buffer.toString('base64')}`
}

module.exports = createPreviewImage

workers/create-preview-video.js is the bare-ffmpeg counterpart for video files. Both preview helpers are worker-side modules (they call bare-media/bare-ffmpeg), so they live alongside the worker in workers/, not in the renderer. Each record stored in the view is { id, name, type, blob, info: { preview, ... } } — small metadata plus a blob id pointing at the full bytes.

Reuse the standard close order

Photo backup does not add an interval or any other timer, so Worker._close keeps the same chain as hello-pear-electron: room → swarm → store. The room itself closes its blob server and blobs core first (VideoRoom._close). The graceful-goodbye handler in workers/index.js fires it.

Render a photo grid

In the renderer, render <img src={entry.info.preview} loading="lazy"> straight from the inline preview, and open the full-size image (or video) via entry.info.link — the blob-server URL the worker attaches in getVideos. Use the renderer's drag-and-drop to call getPathForFile(file) on each dropped file (exposed by the preload bridge) and write { type: 'add-video', path } over the worker pipe, which the worker forwards to its addVideo handler.

Run it

npm run build

# host
npm start -- --storage /tmp/photos-host --name host

Drag and drop photos into the window. The thumbnails appear in the grid immediately. Quit the host, restart it with the same --storage path, and the grid replays from disk.

# friend backing up the same album
npm start -- --storage /tmp/photos-friend --name friend --invite <invite>

The friend's app replicates the room's Hyperblobs and shows the same grid. Originals are downloaded lazily — only the thumbnails are pulled eagerly.

Where to go next

On this page