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 (a stub in the reference app—wire up bare-ffmpeg frame extraction here to generate a video thumbnail). 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 WorkerTask._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