DadanRecorder is the SDK’s headless recorder. It captures the screen, camera or microphone, reports what is happening through events and uploads the result, but it draws nothing. You build the interface. Use it when the built-in studio cannot match your design, or when recording has to fit inside an existing screen.
If you only want to adjust colors or hide a few controls, customizing the built-in studio is less work.
Install
Link to section: Installnpm install @dadan-io/record-sdkDadanRecorder has no React dependency. It is also re-exported by the React, Vue and Angular packages, so you can import it from those if you already use one.
Create a recorder
Link to section: Create a recorderimport { DadanRecorder } from "@dadan-io/record-sdk";
const recorder = await DadanRecorder.create({
publicAppId: import.meta.env.VITE_DADAN_PUBLIC_APP_ID,
mode: "both",
videoTitle: "Walkthrough",
folderId: "123456",
recordingQuality: "1080p",
maxDurationSeconds: 300,
});| Option | Default | Meaning |
|---|---|---|
publicAppId | Your public app ID. The SDK requests an upload token for you | |
mode | "both", "screen", "camera" or "audio" | |
videoTitle | Generated | Title saved with the video |
folderId, teamId | Where the video is saved. folderId wins when both are set | |
recordingQuality | "1080p" | "720p", "1080p" or "2k" |
autoUpload | true | Upload as soon as recording stops. Set false to review first |
maxDurationSeconds | No limit | Stops the recording automatically at this length |
hooks | Functions that can approve or block actions (see below) | |
hookTimeoutMs | 10000 | How long a hook may take before the action is blocked |
create() resolves with the recorder and then emits ready. Subscribing to ready after await still works: late listeners are called once.
Understand the states
Link to section: Understand the statesThe recorder moves through these states, which you can read at any time with recorder.getState():
| State | Meaning |
|---|---|
idle, initializing | Being set up |
ready | Waiting for start(). Also the state after a cancel |
recording, paused | Capturing |
stopping, uploading | Finishing the file and uploading it |
completed | The video is uploaded. getResult() returns the DadanVideo |
error | Something failed. A partial take may still be available |
destroyed | destroy() was called |
Drive your interface from events rather than polling the state.
Record
Link to section: Recordconst preview = document.querySelector<HTMLVideoElement>("#preview")!;
const startButton = document.querySelector<HTMLButtonElement>("#start")!;
const stopButton = document.querySelector<HTMLButtonElement>("#stop")!;
recorder.on("preview.ready", ({ stream }) => {
preview.srcObject = stream;
});
recorder.on("recording.started", () => {
startButton.hidden = true;
stopButton.hidden = false;
});
recorder.on("upload.progress", ({ percent }) => {
stopButton.textContent = `Uploading ${Math.round(percent)}%`;
});
recorder.on("recording.completed", ({ video }) => {
if (video) console.info("Ready to watch:", video.sharedUrl);
});
startButton.addEventListener("click", () => {
void recorder.start();
});
stopButton.addEventListener("click", () => {
void recorder.stop();
});- Call
start()from a click. Browsers only open the screen picker in response to a user action. - Mute the preview. Give the preview element the
muted,autoplayandplaysinlineattributes so it does not echo the microphone and plays on iOS. start()accepts{ mode, cameraDeviceId, micDeviceId }to override the mode or devices for that take.- Other controls:
pause(),resume(),cancel()(discard the take),restart()(new take after a stop) andgetDuration()(elapsed milliseconds).
Let people review before upload
Link to section: Let people review before uploadSet autoUpload: false and the recorder keeps the file after stop(). Show it, then upload when the person confirms:
const reviewRecorder = await DadanRecorder.create({
publicAppId: import.meta.env.VITE_DADAN_PUBLIC_APP_ID,
mode: "camera",
autoUpload: false,
});
async function finishTake(player: HTMLVideoElement) {
await reviewRecorder.stop();
const blob = reviewRecorder.getBlob();
if (blob) player.src = URL.createObjectURL(blob);
}
async function sendTake() {
const video = await reviewRecorder.retryUpload();
console.info("Uploaded", video.id);
}Despite its name, retryUpload() is also how you start the first upload when autoUpload is off. Call URL.revokeObjectURL on the preview URL once you no longer need it.
Choose devices
Link to section: Choose devicesconst { cameras, mics } = await recorder.getDevices();
if (cameras[0]) await recorder.setCamera(cameras[0].deviceId);
if (mics[0]) await recorder.setMicrophone(mics[0].deviceId);
recorder.setMicrophoneEnabled(false);
recorder.setCameraEnabled(true);
recorder.on("devices.changed", ({ cameras, mics }) => {
console.info(`${cameras.length} cameras, ${mics.length} microphones`);
});setMicrophoneEnabledandsetCameraEnabledwork beforestart(). The choice is applied when capture begins.- Muting the microphone in screen modes mutes only the microphone, not the system or tab audio.
- Devices cannot be switched while recording (
cannot_switch_device_while_recording). getAudioLevels()returns{ level }for a microphone meter, andgetPermissions()gives a best-effort view of camera and microphone permissions.
Approve or block actions with hooks
Link to section: Approve or block actions with hooksHooks run before an action and can stop it. Return false, or reject, to block the action:
const guardedRecorder = await DadanRecorder.create({
publicAppId: import.meta.env.VITE_DADAN_PUBLIC_APP_ID,
mode: "screen",
hooks: {
beforeStart: async () =>
window.confirm(
"Recording will include everything on the screen you share. Continue?",
),
beforeCancel: async () => window.confirm("Discard this recording?"),
beforeUpload: async () => navigator.onLine,
},
});| Hook | Runs before |
|---|---|
beforeStart | start(). In screen and both modes it runs after the screen picker, because Safari requires the picker to open straight from the click |
beforeStop | stop(). Skipped when recording stops because the person ended screen sharing |
beforeCancel | cancel() |
beforeUpload | Every upload, including retryUpload() |
Hooks fail closed: if one throws, returns false or takes longer than hookTimeoutMs (10 seconds by default), the action does not happen. When beforeStart blocks, start() rejects with hook_rejected, no error event is emitted, the state returns to ready and any shared screen is released.
Limit the length
Link to section: Limit the lengthWith maxDurationSeconds, the recorder emits recording.limitReached and stops on its own when the limit is reached. Change the limit later with recorder.setMaxDuration(seconds).
Events reference
Link to section: Events referenceSubscribe with recorder.on(name, handler), which returns a function that removes the listener. once and off are also available.
| Event | Payload and meaning |
|---|---|
ready | The recorder is set up |
preview.ready | { stream } for a live preview |
recording.started, recording.paused, recording.resumed, recording.stopped | { recordingId, elapsedMs, timestamp } |
recording.limitReached | The maximum duration was reached |
recording.cancelled | { reason } |
recording.completed | { recordingId, videoId, video } once the upload is done |
recording.error | { code, message, recoverable } |
permissions.denied | { kind }: screen, camera or microphone |
media.trackEnded | A source ended, for example the person clicked Stop sharing |
media.warning | { kind, reason }: a microphone or camera is unavailable or stopped. Recording continues |
microphone.muted, microphone.unmuted | Microphone state changed |
microphone.silenceDetected, microphone.inputRestored | No sound for a while, then sound again |
camera.enabled, camera.disabled | Camera state changed |
devices.changed | { cameras, mics } after a device was plugged in or removed |
upload.started, upload.progress, upload.retrying, upload.finalizing, upload.completed, upload.failed | Upload lifecycle. upload.progress carries { percent, bytesUploaded, totalBytes } |
How uploads behave
Link to section: How uploads behave- Uploads are resumable and retried automatically.
upload.retryingtells you when. - If an upload fails,
stop()rejects with aDadanSdkErrorand the take stays available. CallretryUpload()to try again. cancelUpload(),cancel()anddestroy()stop an upload in progress and delete the unfinished video.- An unfinished take can be kept in the browser for up to 24 hours. See Record SDK production checklist.
Clean up
Link to section: Clean upCall recorder.destroy() when the view that owns the recorder goes away. It releases the camera, microphone and screen, removes listeners and stops any upload in progress.
Go lower level
Link to section: Go lower levelFor complete control, @dadan-io/record-sdk also exports the building blocks DadanRecorder uses, such as startHeadlessCapture, listMediaDevices, attachMediaStream and uploadRecording, plus persistPendingTake and recoverPendingTake for keeping unsent takes. Most apps never need them; start with DadanRecorder and only drop down if you hit a limit.