Build Sendspark

KINDAreplaces $49/mosaves $588/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A local screen and camera recorder whose hard problems are proving you captured the audio you think you captured, producing a file that seeks instead of one the browser refuses to scrub, and building a recipient page that cannot be found by guessing.

estimated effort weekendthe files for this build are in the project pack

RuntimeTauri 2 with React and TypeScript, one desktop applicationCaptureThe display and media capture APIs inside the webview, with tracks verified after the factEncodingffmpeg invoked locally for remux and trim, originals keptStorageSQLite plus a videos directory, no upload anywhereSharingA static page folder with a random identifier, no tracking of any kind

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Builds the desktop application and the native side that shells out to ffmpeg.

    Get it rustup, then your platform's Tauri prerequisites. open ↗

    Verify A Tauri template builds and launches on your machine

  2. installfree

    Why Builds the front end.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  3. installfree

    Why The remux pass is what makes the recording a file that seeks, and ffprobe is how the build verifies it.

    Get it Your package manager, or bundle a static build with the application. open ↗

    Verify ffprobe reports a duration for a remuxed test file

  4. have readyfree

    Why Both are granted outside your application and both fail in ways that look like a bug in your code, usually on the first run after a system update.

    Get it Grant them in the system settings for your build, not only for your development runner.

    Verify A capture from the packaged build returns both a video and an audio track

  5. decidefree to a few pounds a month

    Why A page nobody can reach is not shared, and a page that never expires is a personal video sitting on a disk for years, so this is decided before the page generator exists.

    Get it Decide where the folders are served from, the default expiry, and the identifier length.

    Verify All three appear in the configuration and in the README

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- recordings(id, title, created_at, source_kind, duration_ms, width, height, has_audio, audio_source, raw_path, remuxed_path, sha256, bytes)
- chunks(recording_id, ordinal, path, bytes, written_at) · written to disk as they arrive so a crash keeps what was recorded
- trims(id, recording_id, start_ms, end_ms, created_at, output_path, reencoded) · non-destructive, the original is never replaced
- pages(id, recording_id, public_id, recipient_label, headline, body_md, created_at, expires_at, revoked_at) · public_id is random, not sequential
- artefacts(recording_id, kind, path) · poster frame, transcript, Markdown summary
- settings(storage_ceiling_bytes, default_expiry_days, videos_dir)
- has_audio and audio_source are recorded because the capture API is allowed to hand back a stream with no audio track even when you asked for one, and finding that out after the call is the standard way a recording is wasted.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
VIDEOS_DIRrequired./data/videosChunks, raw concatenations, remuxed files and exports.
DATABASE_PATHrequired./data/recordings.dbRecordings, trims, pages and artefacts.
STORAGE_CEILING_GBrequired20A new recording is refused above this, before capture starts.
FREE_SPACE_MARGIN_GBrequired5Checked alongside the ceiling so the disk is never filled to zero.
PAGE_ID_BYTESrequired16Length of the random page identifier. Longer is cheap here.
DEFAULT_EXPIRY_DAYSrequired30Recipient pages expire and their files are removed.
FFMPEG_PATHrequired/usr/bin/ffmpegResolved once at startup so a missing binary fails loudly, not mid-recording.

The build, in order

  1. Prove you captured what you asked for

    Recording only starts once the tracks that came back are inspected and shown to the user.

    1. The request is a suggestion; the tracks are the fact.

      Files src/capture/start.tssrc/capture/inspect.ts

    2. Two seconds of checking against forty minutes of silent video.

      Files src/components/InputMeter.tsx

    3. Source kind, audio source, dimensions, at capture time.

      Files src/db/recordings.tstests/capture.test.ts

    done when · tick each as it passes
    watch out
    • The documented behaviour is that the returned stream may contain no audio track even when audio was requested and system audio was asked to be included, and that the options you pass cannot limit what the user picks because they are applied after the choice is made. So a recorder that trusts its own request records forty minutes of silent demo and you find out when you open it. Inspect the tracks after the promise resolves, show a meter, and block the start on a silent input.
  2. A file that survives a crash and can be scrubbed

    Chunks land on disk as they arrive and the finished file is remuxed so players can seek it.

    1. Memory is where recordings go to die at minute thirty nine.

      Files src/capture/chunks.ts

    2. Otherwise the player cannot seek and reports no duration.

      Files src-tauri/src/encode.rstests/remux.test.ts

      terminal
      ffmpeg -i raw.webm -c copy -fflags +genpts fixed.webm
      ffprobe -v error -show_entries format=duration fixed.webm
    3. The crash already happened; do not also lose the footage.

      Files src/capture/recover.ts

    done when · tick each as it passes
    watch out
    • Buffering the whole recording in memory and writing it at the end means a crash at minute thirty nine loses the entire session, and the output from a browser recorder typically lacks the duration and index a player needs, so even a successful recording scrubs to nowhere and reports an unknown length. Write chunks as they arrive and always run a remux pass before the file is treated as finished.
  3. Trimming that does not touch the original

    Cuts are stored as ranges, exports are rendered from them, and the source file is never replaced.

    1. Non-destructive is the whole reason the original is kept.

      Files src/trim/model.ts

    2. An inaccurate cut presented as exact is the annoying kind of bug.

      Files src-tauri/src/encode.rstests/trim.test.ts

    3. The first frame of the source is usually a desktop.

      Files src/trim/poster.ts

    done when · tick each as it passes
    watch out
    • Cutting with a stream copy at an arbitrary timestamp silently starts the output at the previous keyframe, so the export begins a second and a half before the point you chose and includes the throat clearing you were removing. Either snap the cut to keyframes and show the user where it actually landed, or re-encode and say so, but never present an inaccurate cut as an exact one.
  4. A page for one person, not a directory for everyone

    Each recipient page has an unguessable identifier, an expiry and a revoke, and tracks nothing.

    1. Not a counter, not a hash of the recipient's email.

      Files src/pages/id.tstests/page-id.test.ts

    2. Fonts and analytics included, which means self-hosting both or neither.

      Files src/pages/render.ts

    3. A flag on a static file server protects nothing.

      Files src/pages/revoke.ts

    done when · tick each as it passes
    watch out
    • Numbering pages sequentially or deriving the identifier from the recipient's name or email means one prospect can find another prospect's page by changing a character, and since these pages are usually personal sales messages the leak is the embarrassing kind. Use a long random identifier from a cryptographic source and never derive it from anything about the recipient.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more screen recording.