Integrate the Flipbase Recorder (V2)
- Getting started
- If your page loads an unversioned bundle
- Settings
- The callback
- Upload events
- The recorder instance
- Multiple recorders on a single page
- What you should know before you implement the recorder
- Browser support
Getting started
In order to use the Recorder V2, place the HTML-code in your page as shown below. Load the recorder.js library and initialize the recorder against an HTML element using its id.
There is one recorder code line, and it continues as 2.x. Every release is published to its own path, which never changes once it exists — so pin the version you tested against, as the snippet below does.
Load from cdn.flipbase.com. A versioned path exists only there — app.flipbase.com does not serve one, so swapping the host turns a working embed into a 403. The old unversioned paths are the exception: they are served by both hosts, and an existing integration pointing at app keeps working exactly as it does today.
<!-- Load the JavaScript Recorder V2 library, pinned to an exact version.
A version path is immutable: these bytes will not change under you. -->
<script src="https://cdn.flipbase.com/recorder/v2.0.0-alpha.2/recorder.js"></script>
<!-- Place an element at the spot where you would like to show the Recorder -->
<div id="recorder1"></div>
<!-- Initialize the Recorder and provide your 'recorderId' -->
<script>
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407'
});
</script>
Opens in a new tab. Runs the published bundle from the CDN — the same bytes you would embed.
When the user has successfully saved the video, your callback receives the video's UUID. Store it in your database to play back the video later using the Flipbase Player.
If your page loads an unversioned bundle
This page documents the same recorder API that the older unversioned
recorderv2 bundle serves, so all of it applies to you unchanged — only the
script URL differs. These are the two unversioned URLs to recognise:
| What your page loads | Global | Documented on |
|---|---|---|
https://cdn.flipbase.com/recorder/recorder/recorderv2/recorder.js | FlipbaseV2 | this page |
https://cdn.flipbase.com/recorder/recorder.js | Flipbase | Recorder V1 |
Both are frozen. They keep serving the bytes they serve today and they never receive a fix, because changing them would change what every page already loading them gets. Pin a version — the snippet above — to receive fixes.
One thing about them is not frozen: each fetches its stylesheet at runtime from
an unversioned URL on app.flipbase.com
(/recorder/css/style.css for the older bundle,
/recorder/recorderv2/recorder/css/style.css for this one), whichever host the
script itself came from. So an unversioned integration depends on that host
being reachable, and its appearance can change when that stylesheet is
republished even though the script cannot. A versioned release has no such
thread: its stylesheet sits inside the version folder next to the bundle, and
is pinned by the same URL.
recorder twicehttps://cdn.flipbase.com/recorder/recorder/recorderv2/recorder.js is the
correct URL for the unversioned bundle. The repeated segment looks like a
mistake and is not: shortening it to /recorder/recorderv2/ returns 403,
and an embed that was working stops working.
https://app.flipbase.com/recorder/recorderv2/recorder.js serves the
byte-identical bundle if you would rather load it from the application host.
The versioned paths do not repeat the segment, and they are not served by
app.flipbase.com at all — https://cdn.flipbase.com/recorder/v2.0.0-alpha.2/recorder.js
is the whole URL.
Settings
The recorderId and selector are required. All other parameters listed below are optional.
| Parameter | Type | Default | Description |
|---|---|---|---|
| recorderId | String | — | UUID provided by Flipbase. Required. |
| selector | String | — | The id of the element to mount the recorder into. Required. |
| duration | Number | 30 | Maximum duration in seconds of a single take. |
| locale | String | nl-NL | See supported languages below. |
| maxWidth | Number | — | Maximum width in pixels. The recorder always maintains a 16:9 aspect ratio — it picks the most conservative dimension. |
| maxHeight | Number | — | Maximum height in pixels. See maxWidth description. |
| primaryColor | String | #a62651 | Color of the most prominent buttons. HEX format. |
| secondaryColor | String | #ffffff | Secondary button color. HEX format. |
| backgroundColor | String | #ffffff | Background color of the application. HEX format. |
| textColor | String | #232322 | Text color. HEX format. |
| allowRecordMethod | Boolean | true | Whether the home screen offers Record. See Which methods the candidate is offered. |
| allowUploadMethod | Boolean | true | Whether the home screen offers Upload a video. See Which methods the candidate is offered. |
| maxTakes | Number | 0 | How many webcam takes the candidate gets. 0 means unlimited. See Takes. |
| showTakesCounter | Boolean | follows maxTakes | Whether the "takes remaining" line is printed. See Takes. |
| recordingQuality | String | auto | Caps the resolution and bitrate of a webcam recording. See Recording quality. |
| persistTake | Boolean | true | Keep a webcam take in IndexedDB until its upload succeeds, so a refresh mid-upload does not lose it. See Surviving a refresh. |
| takeKey | String | derived | Which stored take belongs to this recorder. See Surviving a refresh. |
| warnBeforeLeaving | Boolean | true | Confirm before the page is left with an upload in flight. See While an upload runs. |
| uploadStallWarningSeconds | Number | 20 | After how many seconds of no bytes the upload screen tells the candidate the transfer has stopped. 0 or false turns the message off. See While an upload runs. |
| minUploadScreenMs | Number | 900 | The shortest time the upload screen stays up, so a fast upload is not a flicker. Holds the screen only, never the transfer or your handlers. 0 turns it off. |
| exitFullscreenOnSave | Boolean | true | Leave full screen when the candidate saves their take. See Full screen. |
| warnOnSilentMic | Boolean | 'always' | true | Warn the candidate when their microphone is picking up nothing. See Silent microphones. |
| allowDeviceSwitch | Boolean | true | Let the candidate change microphone and camera from the navigation bar. See Silent microphones. |
| prompt | Object | — | The question the candidate is answering: {title, tips: []}, both optional. Shown in full screen only. See The question on screen. |
| output | String | — | Decides what the callback's first argument contains. Leave undefined for the video's UUID. Set to embedCode for an embeddable iframe instead — it replaces the UUID, it is not an extra argument. |
| outputOptions | Object | — | Object with optional pageName and privacy properties. pageName is required when output is embedCode. |
| callback | Function | String | — | Called when the upload finishes. Invoked with (output, element) — see The callback. A string is treated as the name of a function on window. |
| onUploadStart onUploadProgress onUploadSuccess onUploadError onUploadAbort onTakeRestored | Function | String | — | Follow an upload as it runs. See Upload events. |
Every boolean above is read strictly: only an explicit false takes a feature
away, so an options object built from a form that hands over undefined keeps
the default rather than quietly switching something off.
showFAQButton is retiredIt switched a question-mark icon that opened a page which has answered 404 for
years, and it was read inverted on top of that — the documented default true
hid the icon. The option is now accepted and ignored: passing it is harmless,
nothing a candidate sees changes either way, and the recorder logs one
console.warn naming it so it can be taken out of your configuration whenever
you next touch it.
The callback
The option is named callback. There is no onSubmit — a recorder initialised
with one registers no callback at all and never tells you the video exists.
It is called once the upload has finished, with two arguments:
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
callback: function (output, element) {
// `output` is a string, not an object. With no `output` option set it is
// the video's UUID — this is the value to store.
// `element` is the DOM element the recorder was mounted into.
console.log('video UUID:', output);
},
});
| Argument | Type | What it is |
|---|---|---|
output | String | The video's UUID, or the embed code when output: 'embedCode' is set. |
element | HTMLElement | The element the recorder was mounted into. |
The first argument is a string, so output.uuid is undefined; use the
value itself.
callback also accepts a string, which is treated as the name of a function on
window and called with the same two arguments.
Flipbase and FlipbaseV2 are the same objectThe bundle assigns window.FlipbaseV2 and then window.Flipbase = window.FlipbaseV2,
so Flipbase.recorder(...) and FlipbaseV2.recorder(...) call the same
function. Examples here use FlipbaseV2; existing integrations on Flipbase
do not need changing.
Which methods the candidate is offered
The home screen shows one prominent Record button with the maximum length
under its label, and Upload a video underneath it. Both are on by default,
and an explicit false removes one — removes, not hides, so what is left is
the only thing in the frame.
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
allowUploadMethod: false, // record only
});
Setting both to false would leave an empty frame, so that is ignored and both
are offered.
There are two methods and not three: recording covers the webcam and the
phone's own camera, and which one is reached is the recorder's decision rather
than a question put to the candidate. Where the browser has getUserMedia and
MediaRecorder, Record opens the webcam — on a phone as well as on a
desktop. Where it does not, which means an old browser or an insecure origin,
Record falls back to the phone's own camera app, and on a desktop to the file
picker. That last case is the one place a candidate reaches a file picker on an
integration that passed allowUploadMethod: false; the alternative is a button
that cannot work. Whichever path is taken, the method reported to your handlers
is still 'webrtc', 'mobile' or 'upload'.
Takes
maxTakes limits webcam takes. A take is spent when the countdown starts,
not when the recording stops — a candidate who sees themselves on camera and
backs out has had the attempt, and counting at the end would let them restart
endlessly by abandoning each one.
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
maxTakes: 2,
});
The count lives in localStorage keyed by recorderId, so a refresh does not
hand out a fresh set. Two recorders created with the same recorderId on one
page therefore share a limit — give them different ids if each should have its
own — and resetTakes() clears the count when you invite the same
candidate to answer again.
The phone-camera fallback and the file upload are never limited. The candidate hands over a finished file and there is no take to count.
showTakesCounter decides whether the "takes remaining" line is printed, under
the record button and under the after-take menu:
FlipbaseV2.recorder({ maxTakes: 3 }) // "Takes remaining: 2"
FlipbaseV2.recorder({ maxTakes: 3, showTakesCounter: false }) // nothing
FlipbaseV2.recorder({ maxTakes: 0, showTakesCounter: true }) // "unlimited takes"
FlipbaseV2.recorder({ maxTakes: 0 }) // nothing
It hides the line and not the limit: with showTakesCounter: false the
candidate still gets three takes and the re-record button still goes dead once
they are gone. Use it where your own page already says how many attempts there
are, or where a counter would only add pressure.
Recording quality
How large a webcam recording is allowed to be — auto (the default), 1080p,
720p, 480p or 360p. A level sets the resolution asked of the camera and
the bitrate given to MediaRecorder, because either alone does very little.
| Level | Asked of the camera | Video bitrate | 30 seconds at most |
|---|---|---|---|
auto | nothing | the browser's own default | no ceiling |
1080p | 1920x1080, 30 fps | 4 Mbps | 15 MB |
720p | 1280x720, 30 fps | 2 Mbps | 8 MB |
480p | 854x480, 30 fps | 1 Mbps | 4 MB |
360p | 640x360, 24 fps | 0.6 Mbps | 3 MB |
Two things to know before picking one. The resolution is a preference and not a
demand — every dimension is sent as ideal, so a camera that cannot produce it
negotiates something close rather than failing. And 1080p or 720p can make
a recording larger than auto on a browser whose unconstrained default is
smaller, Chrome's being 640x480: they cap the bitrate and raise the frame size.
Choose 480p or 360p to be sure of a smaller file.
An unknown value logs a warning naming the levels and records at auto, so a
typo cannot stop somebody recording. A bare number is accepted, so 720 reads
as '720p'.
It applies to the webcam method only. On the phone-camera fallback the phone's own Camera app makes the recording, at whatever the phone is set to, and a 30-second clip from a handset shooting 4K is several hundred megabytes — nothing any recorder option can change.
Surviving a refresh
A webcam take is written to IndexedDB as it is recorded, and a recorder that
comes up on a page where one is waiting restores it rather than starting empty.
The candidate lands back on play / re-record / save, and
onTakeRestored tells your page it happened. This is on by
default; persistTake: false turns it off and writes nothing.
Worth knowing:
- Saving a restored take uploads it from the beginning. The interrupted upload is not resumed and nothing in the interface suggests it is.
- The webcam method only — the phone-camera fallback hands over a file of hundreds of megabytes, which is not something to copy onto the phone's own storage while an upload is already struggling.
- A stored take is deleted the moment its upload succeeds, and kept when an upload fails or is cancelled, which is when the candidate needs it. Anything older than 24 hours is swept on the next page load.
- A recording cut off mid-sentence by the reload is not restored, and nothing is restored over an element that already carries a submitted value.
- Where there is no IndexedDB — private browsing, blocked site data, an exceeded quota — every operation is a no-op and nothing is said to the candidate, because nothing has gone wrong with their recording.
takeKey says which stored take belongs to which recorder. Left alone it is
derived from the recorderId, the element, location.pathname and an id for
the browsing session, which means a take is recoverable across a refresh of the
same tab and not beyond it. That session boundary is deliberate: on a machine
more than one person uses, everything else in the key describes the page, so
two candidates applying for the same vacancy would otherwise produce the same
key and the second would be handed the first one's recording.
One case the recorder cannot work out for itself is two applications reached at the same path, through the same recorder, in one browsing session. Pass the application's own id there:
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
takeKey: 'application-4711',
});
A takeKey you pass is used exactly as given, without the session part — so
make sure it identifies the application and not just the page.
While an upload runs
Three things guard a candidate on a bad connection who has already recorded.
The screen is held awake where the browser has
navigator.wakeLock,
because a phone that locks its screen mid-upload freezes the socket rather than
closing it. There is no option for this: the lock is taken when the transfer
starts and released however it ends. If you embed the recorder in an iframe, add
screen-wake-lock to the frame's allow attribute, or the request is refused
there.
Leaving the page asks for confirmation while a transfer is in flight, since
leaving loses the take. The wording is the browser's own; nothing you pass is
shown. Pass warnBeforeLeaving: false if your own page already asks, because
two prompts in a row are worse than one.
A transfer that stops moving says so, after uploadStallWarningSeconds, and
offers the candidate a way out. The transport is still retrying underneath that
message — see fatal in Upload events.
minUploadScreenMs is unrelated to all three: it keeps the upload screen up for
at least 900 ms so that a fast upload registers as a screen rather than a
flicker. It holds the interface only. The transfer is not slowed and neither is
callback or onUploadSuccess.
Full screen
The navigation bar carries a full-screen button, and the recorder goes full screen by itself on a phone or tablet when the candidate starts recording.
This is not the browser's Fullscreen API, which needs a user gesture, is
refused inside a cross-origin iframe without an allow attribute, and on iOS
Safari does not exist for anything but a <video>. The recorder instead fixes
itself over the viewport with position: fixed and a z-index of 9999, which
works everywhere it is embedded — a page whose own stacking puts something above
that z-index will see it overlap.
Full screen also sets document.body.style.overflow = 'hidden'. That is the one
thing the recorder changes outside its own subtree, and your page's own value is
saved and put back rather than deleted.
Saving a take leaves full screen, and the moment it happens is the
candidate's tap rather than the end of the upload — so a failed, cancelled or
still-running upload all land out of full screen, with the take in front of the
candidate at the recorder's embedded size. Pass exitFullscreenOnSave: false
only if your page manages full screen itself; the failure it guards against is a
candidate left on a page they cannot scroll, unable to reach the rest of your
form.
Silent microphones, and changing device
warnOnSilentMic measures the live microphone while the camera is open and,
after about three unbroken seconds of near-silence, shows a bar reading "No
sound detected. Please check your microphone settings." It appears before
recording, while the candidate has not yet spent a take, and during a recording,
because a headset can be muted at any point.
It warns and does nothing else. It cannot stop a recording, a save or an upload, and the candidate can close it. Three values:
| Value | What it does |
|---|---|
true | The default. Measures where the level reading is worth trusting. |
'always' | Measures on Apple engines too, where it is suppressed by default because a flat-zero reading there produces false positives for working microphones. Pass it if you measure your own Apple traffic and know what it does. |
false | Off entirely. No AudioContext is created and nothing is measured. |
allowDeviceSwitch is the other half of it: the microphone and camera buttons
in the navigation bar, which let the candidate record on a different input.
Telling someone their microphone is picking up nothing and offering no way to
change it is half a feature.
The buttons appear only once the camera is open, only before a take, and only
when the browser reports more than one input of that kind — enumerateDevices()
answers before permission with blank names, a switch replaces the stream the
MediaRecorder is attached to, and a menu with one item in it is noise. What a
candidate chooses is kept for that recorder and not written to storage, so the
next page load starts from the browser's own choice again.
Pass allowDeviceSwitch: false where the inputs are not the candidate's to
choose — a kiosk with a fixed camera rig. Nothing is listed at all then.
Both apply to the webcam method only: the other two methods hand over a finished file, and there is no live microphone to measure.
The question on screen
The question the candidate is answering, shown in full screen only — which is the problem it exists for, the recorder covering the page the question was printed on.
var recorder = FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
prompt: {
title: 'Tell us about a time you changed someone\'s mind at work',
tips: ['Keep it to one example', 'Say what changed afterwards'],
},
});
// A screening that asks several questions through one recorder
recorder.setPrompt({ title: 'Question two' });
Both parts are optional, and leaving prompt out renders nothing. Beside an
embedded recorder you already have your own page to put the question on.
Full example
An example of a fully customized recorder:
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
duration: 15,
allowUploadMethod: false,
maxTakes: 3,
primaryColor: '#aeb00a',
secondaryColor: '#e0e0e0',
backgroundColor: '#242b3c',
textColor: '#FFFFFF',
locale: 'en-US',
recordingQuality: '480p',
takeKey: 'application-4711',
output: 'embedCode',
outputOptions: {
pageName: 'my-page',
privacy: 'public',
},
maxWidth: 640,
callback: function (output, element) {
// `output: 'embedCode'` is set above, so `output` here is the embed code,
// not the UUID.
console.log('Video saved. Embed code:', output);
},
onUploadError: function (info) {
if (info.fatal) showError(info.reason);
}
});
Upload events
callback tells you a video exists. These tell you how the upload is going,
which is what a page needs to disable a submit button, show its own progress, or
log a failure.
| Handler | When it fires | info carries |
|---|---|---|
onUploadStart | The file has been accepted and the transfer begins, before the video document exists. | method ('webrtc', 'upload' or 'mobile'), bytesTotal |
onUploadProgress | Every progress event from the transfer. | percentage (0–100, rounded), bytesSent, bytesTotal |
onUploadSuccess | The upload finished. Fires after callback, and after isUploading() has gone false, so a handler may submit your form. | videoId, output |
onUploadError | The upload failed, the file was refused, or the transfer has stopped moving. | reason, fatal, error |
onUploadAbort | The upload was cancelled, by the recorder's own cancel button or by abort(). | — |
onTakeRestored | A webcam take from a previous page load was recovered, shortly after the recorder is created. Not an upload event — see Surviving a refresh. | method (always 'webrtc'), bytesTotal, duration (seconds), recordedAt (ms since the epoch) |
Every handler is optional, and every one takes either a function or the name
of a function on window, the same as callback always has — the name is
looked up when the event fires, so a global defined further down the page still
works. A handler that throws is logged and otherwise ignored: a mistake in your
form validation cannot break an upload or stop the other handlers.
Exactly one of onUploadSuccess, onUploadError and onUploadAbort ends an
upload, and nothing fires after it. A new take is a new upload and reports again
from the start.
reason, and the one error that is not the end
reason is one of invalid_file, image_provided, invalid_file_size,
timeout, upload_failed or upload_stalled. The first three are files the
recorder refuses, and they arrive without a preceding onUploadStart,
because no transfer ever began. error is the underlying error, whose shape is
not part of this contract.
upload_stalled is the exception: fatal is false on it and true on every
other onUploadError. It means the transfer has delivered no bytes for
uploadStallWarningSeconds and the candidate has been shown a way out — the
transport is still retrying underneath it, so a success, an abort or a fatal
error is still to come. It can fire more than once if an upload recovers and
stops again. Branch on fatal if you are counting failures:
FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
onUploadProgress: function (info) {
bar.style.width = info.percentage + '%';
},
onUploadError: function (info) {
if (!info.fatal) return track('upload stalled', info); // still going
showError(info.reason);
},
onUploadSuccess: function (info) {
form.submit(); // the UUID is already on the element
},
});
window._fbwindow._fb and window.FlipbaseV2.RecordersStore are the internal store, on
window for historical reasons rather than as an API. addChangeHandler on
them holds one handler per recorder and replaces it silently — and the handler
it replaces is the recorder's own view, so a page that subscribes
unsubscribes the interface: it renders once and then never updates again,
with nothing thrown and nothing logged. The events above exist so there is no
reason to go near it.
The recorder instance
FlipbaseV2.recorder(...) returns an object with six methods.
| Method | What it does |
|---|---|
isUploading() | true while a video is uploading — not while recording. Requires selector to be set. |
abort() | Cancels the upload in flight and tells S3 to release the parts. |
resume() | Starts an interrupted upload again, keeping the parts S3 has already acknowledged. |
resetTakes() | Clears the take count, giving the candidate their takes back. See Takes. |
setPrompt(prompt) | Replaces the question on screen. See The question on screen. |
destroy() | Takes the recorder off the page. |
var recorder = FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
});
console.log('Is uploading?', recorder.isUploading()); // false
abort() and resume() are for a page with its own "start over" or "retry"
button, which cannot press the one inside the interface. Both are safe to call
at any time and do nothing when no upload is in flight. resume() is rarely
needed: the uploader already resumes itself when the page becomes visible again
or the connection comes back, and the try again button on a stalled upload
calls the same thing.
destroy()
Removes the interface and its <style> element, stops the camera and the
MediaRecorder, abandons an upload in flight, and releases every listener and
timer the recorder took out — the resize handler, the uploader's
visibilitychange and online handlers, the leave-page confirmation and the
screen wake lock. Your own element is left where it was, still holding the
value your page submits. A webcam take kept under persistTake is deliberately
not deleted: a page being torn down is the case that exists to survive.
It is callable at any moment and more than once — a second call does nothing — including from inside a handler the recorder has just called, which is how a framework integration always reaches it:
const recorder = FlipbaseV2.recorder({
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
selector: 'recorder1',
callback: (output) => setVideoId(output), // React unmounts us from here
});
useEffect(() => () => recorder.destroy(), []);
By the time any handler is called the recorder has finished with that take: the
output is on the element, the interface is in its finished state and
isUploading() is false. A recorder torn down while in full screen gives your
page its body scrolling back, even when the page removed the recorder's
elements first — the order most frameworks unmount in.
Multiple recorders on a single page
Each recorder instance requires a unique selector. Use the returned object's destroy method to remove an instance.
<div id="recorder1"></div>
<div id="recorder2"></div>
<script>
var rec1 = FlipbaseV2.recorder({
selector: 'recorder1',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
primaryColor: '#aeb00a',
});
var rec2 = FlipbaseV2.recorder({
selector: 'recorder2',
recorderId: '9eaf41fd-4f3f-4fdb-b8ca-de84eeaed407',
});
// Destroy an instance when you no longer need it
// rec1.destroy();
</script>
Two recorders sharing a recorderId, as above, share one take count — see
Takes. Give them different ids if each should have its own. They are
separated for persistTake without anything being passed,
because the element is part of the derived key.
Limitations
- Keep the recorder interface at least 244 pixels wide at all times, including on mobile.
- The recorder is fully responsive and fills its parent element by default. Use
maxWidthto constrain it. - If the parent element is a
<table>, the table must have a minimum width set, otherwise webcam recording will not work. - The recorder always maintains a 16:9 aspect ratio.
Browser support
Supported languages
Below you will find a list of all supported languages and their locale configuration property values.
- Dutch: nl-NL, nl-BE, nl
- English: en-US, en-GB, en-AU, en-CA, en-NZ, en-JM, en-ZA, en-IE, en
- German: de, DE, de-CH, de-AT, de-LU, de-LI
- Spanish: es, es-ES, es-CR, es-GT, es-PA, es-DO, es-MX, es-VE, es-CO, es-PE, es-CL, es-EC, es-UY, es-PY, es-BO, es-SV, es-HN, es-NI, es-PR
- Portugues: pt, pt-PT
- Portuguese Brazil: pt-BR
- French: fr, fr-BE, fr-CA, fr-MC, fr-CH, fr-LU, fr-FR
- Italian: it, it-IT, it-CH
- Turkish: tr, tr-TR
- Czech: cz, cz-CZ
- Thai: th
- Arabic: ar, ar-SA, ar-IQ, ar-EG, ar-LY, ar-DZ, ar-MA, ar-TN, ar-OM, ar-YE, ar-SY, ar-JO, ar-LB, ar-KW, ar-AE, ar-BH, ar-QA
- Polish: pl
- Slovakian: sk
- Ukranian: uk
- Russian: ru
- Indonesian: id
- Simplified Chinese: zh-CN
- Japanese: ja
- Swedish: sv
- Serbian: sr
- East Armenian: hy
- Kirghiz: ky
- Romanian: ro, ro-RO
What the recorder requires
The recorder decides what to offer from what the browser actually has, at runtime, rather than from a version list — so this is the list of capabilities it tests.
Webcam recording needs MediaRecorder, getUserMedia, native promises and
Blob, plus the file-upload set below. It also needs a secure origin:
browsers only grant camera access over HTTPS, with localhost counting as
secure. In practice that is any current Chrome, Edge, Firefox, Safari or Opera
on the desktop, and iOS Safari 14.3 or newer on a phone.
File upload needs FormData, XMLHttpRequest.upload and
URL.createObjectURL, which every browser still receiving updates has.
Where webcam recording is unavailable — an old browser, or any page served over plain HTTP — the recorder does not fail. Record falls back to the phone's own camera app on a phone, and to the file picker on a desktop, where there is no camera for the browser to reach at all. See Which methods the candidate is offered.
Features that degrade quietly
Each of these checks for itself and does nothing where it is unavailable. None of them stops a candidate recording or submitting.
| Feature | Needs | Without it |
|---|---|---|
persistTake | IndexedDB | Nothing is stored, and nothing is said — private browsing and blocked site data are the usual cases. |
warnOnSilentMic | Web Audio | No measurement, no warning. Suppressed on Apple engines unless you pass 'always'. |
allowDeviceSwitch | enumerateDevices, and more than one input | No buttons in the navigation bar. |
| The screen wake lock | navigator.wakeLock | The upload runs exactly as before. Add screen-wake-lock to the allow attribute if you embed the recorder in an iframe. |
Flash is not involved anywhere. Browsers removed the plugin in January 2021 and nothing in this line asks for it.