Record from a browser
@revero/recorder records microphone audio into durable local chunks and
uploads it only after explicit confirmation. It never accepts a Revero OAuth
client secret or service access token. Its adapter calls your backend for
short-lived instructions.
Ready-made React component
Every state renders one control: start while idle, pause or stop while recording, resume while paused, confirm or discard once stopped, confirm again after a failed upload, and record again once submitted. That last one resets the recorder to idle, so one mounted component records as many times as the person needs.
Write the adapter inline if that reads best. The recorder is created once per
mount and reads its options at call time, so a re-render of the surrounding
component cannot restart a recording in progress. The other side of that:
swapping store in place is not a reset. To record against a different
meeting, remount with a React key, as above.
For anything the component does not render, take the controller itself:
A meeting that takes a break
pause() holds the recording open without capturing. resume() continues it.
The audio is one continuous file with the break simply absent, which means one
meeting, one transcript and no gap in the middle.
Prefer that to stopping and starting again. A Revero meeting carries one recording, so two sittings are two meetings you then have to reconcile yourself. See Model your meetings.
While paused, status stays "recording" and paused is true.
How long has this been running
elapsedMs(state, now) is a pure function, exported from
@revero/recorder/core. It is not a field on the state, because a duration
that ticks would publish a new snapshot several times a second and re-render
every consumer with it. Own the interval, and call this from it:
What you cannot reconstruct yourself is where the pauses were. That is what the function reads.
A level meter
controller.stream is the live MediaStream while one is open. Attach your own
AnalyserNode to it rather than opening a second getUserMedia:
Do not stop its tracks. The recorder owns their lifetime and releases them on
stop() and discard().
Failures carry a code
state.error is a human sentence we reword; state.errorCode is the contract.
Branch on the code, and translate it yourself.
A refused microphone is state, not an exception you have to catch: start()
rejects, and the recorder also lands on idle with the code set, so the
component shows it without any work from you.
One distinction worth making in your own wording. recording_interrupted
shares a state with the four codes that imply a previous attempt
(upload_interrupted, prepare_failed, upload_failed, complete_failed),
but it means something different: a restored recording has never been sent at
all. “Send again” is wrong for it. Give those four your retry wording, and give
recording_interrupted a first-send wording of its own.
upload_failed with nothing in your server log
If the upload appears in no log on either side, the request never left the
browser. @revero/recorder sets Content-Type on the PUT, which is not a
CORS-simple request, so the browser preflights the storage host first, and a
refused preflight reaches you as upload_failed and Failed to fetch.
Confirm it with the preflight itself, from a terminal:
A 200 carrying an Access-Control-Allow-Origin header means the browser path
is open and the fault is elsewhere. Anything else is a storage-side CORS
refusal: tell us your exact origin, because that is what the allowlist keys on.
Note that the same PUT succeeds from curl even when no browser can make it,
because curl does not preflight. A successful curl upload is therefore not
evidence that the browser path works, and that asymmetry has hidden this
failure before.
An interrupted recording comes back
Chunks live in IndexedDB, so a closed tab, a reload or a flat battery does not lose the audio. On mount the recorder restores whatever it finds:
- a recording that never stopped comes back as
stoppedwithrecording_interrupted - a transfer that never completed comes back as
failedwithupload_interrupted, and confirming again resumes it
Restored state carries restored: true and the original startedAt, so you can
say we found an unfinished recording from 14:32 rather than showing a bare
send button with no explanation.
About the idempotency key
The adapter hands your backend an idempotencyKey. It is the recorder’s own
session id: stable for one recording, held in IndexedDB, and it survives the
browser being closed and reopened.
That is exactly the property a replay key needs, and it is why the browser supplies it rather than your backend minting one. Your backend still owns the decision: persist the key alongside the completion command, and send that same key if you have to replay. Nothing forces you to use ours, but a key your backend invents after a restart cannot replay a command sent before it.
See Reliability and recovery for replay against retry.
Headless interface
Use useReveroRecorder(options) from @revero/recorder/react for a custom
React interface, or createRecorder(options) from @revero/recorder/core when
the UI is not React. Both interfaces use the same state machine and
RecorderStore contract, and both expose pause, resume and the controller.
Support matrix
The release gate builds a clean Next.js fixture, imports all four package entrypoints during SSR, and exercises the user-visible recorder actions in Chromium, Firefox and WebKit.