Build Sendspark
KINDAreplaces $49/mosaves $588/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - installfree
Why Builds the front end.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
VIDEOS_DIR | required | ./data/videos | Chunks, raw concatenations, remuxed files and exports. |
DATABASE_PATH | required | ./data/recordings.db | Recordings, trims, pages and artefacts. |
STORAGE_CEILING_GB | required | 20 | A new recording is refused above this, before capture starts. |
FREE_SPACE_MARGIN_GB | required | 5 | Checked alongside the ceiling so the disk is never filled to zero. |
PAGE_ID_BYTES | required | 16 | Length of the random page identifier. Longer is cheap here. |
DEFAULT_EXPIRY_DAYS | required | 30 | Recipient pages expire and their files are removed. |
FFMPEG_PATH | required | /usr/bin/ffmpeg | Resolved once at startup so a missing binary fails loudly, not mid-recording. |
The build, in order
Prove you captured what you asked for
Recording only starts once the tracks that came back are inspected and shown to the user.
The request is a suggestion; the tracks are the fact.
Files
src/capture/start.tssrc/capture/inspect.tsTwo seconds of checking against forty minutes of silent video.
Files
src/components/InputMeter.tsxSource kind, audio source, dimensions, at capture time.
Files
src/db/recordings.tstests/capture.test.ts
done when · tick each as it passeswatch 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.
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.
Memory is where recordings go to die at minute thirty nine.
Files
src/capture/chunks.tsOtherwise the player cannot seek and reports no duration.
Files
src-tauri/src/encode.rstests/remux.test.tsterminalffmpeg -i raw.webm -c copy -fflags +genpts fixed.webm ffprobe -v error -show_entries format=duration fixed.webm
The crash already happened; do not also lose the footage.
Files
src/capture/recover.ts
done when · tick each as it passeswatch 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.
Trimming that does not touch the original
Cuts are stored as ranges, exports are rendered from them, and the source file is never replaced.
Non-destructive is the whole reason the original is kept.
Files
src/trim/model.tsAn inaccurate cut presented as exact is the annoying kind of bug.
Files
src-tauri/src/encode.rstests/trim.test.tsThe first frame of the source is usually a desktop.
Files
src/trim/poster.ts
done when · tick each as it passeswatch 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.
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.
Not a counter, not a hash of the recipient's email.
Files
src/pages/id.tstests/page-id.test.tsFonts and analytics included, which means self-hosting both or neither.
Files
src/pages/render.tsA flag on a static file server protects nothing.
Files
src/pages/revoke.ts
done when · tick each as it passeswatch 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.
Disk, deletion and getting a video outproduct builder
Storage has a ceiling, old recordings expire, and any recording leaves as a self-contained folder.
Failing at minute nought is far better than at minute thirty.
Files
src/storage/guard.tstests/storage.test.tsIn one operation, with a deletion log.
Files
src/storage/retention.tsVideo, poster, transcript, summary, metadata.
Files
src/export/bundle.ts
done when · tick each as it passeswatch out- A recorder with no storage ceiling fills a laptop disk quietly over three months, and the failure arrives as an unrelated crash in a different application while a recording is in progress. Check free space and the configured ceiling before starting, and refuse with a list of the largest recordings rather than starting and failing midway.
That is the whole plan for Sendspark. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Hosted sharing on a public domain.
- Viewer identity and engagement tracking.
- Bulk personalised video from a spreadsheet.
- Team libraries, seats and workspaces.
- personalization at scale, CRM workflows, hosting, and engagement tracking
- instant hosted sharing
- viewer analytics
- team libraries
- cross-device capture
- A local transcript pass over the audio, which makes old recordings findable without any upload
- A second camera track composited in the corner, once the single-track path is reliable
- A per-page short note the recipient can reply to by email, which is the only feedback worth having without tracking
Need the files? The project pack on the verdict page hands your agent the whole brief · more screen recording.