Build a custom recorder with DadanRecorder

On this page
  1. Install
  2. Create a recorder
  3. Understand the states
  4. Record
  5. Let people review before upload
  6. Choose devices
  7. Approve or block actions with hooks
  8. Limit the length
  9. Events reference
  10. How uploads behave
  11. Clean up
  12. Go lower level

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.

bash
npm install @dadan-io/record-sdk

DadanRecorder 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.

typescript
import { 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,
});
OptionDefaultMeaning
publicAppIdYour public app ID. The SDK requests an upload token for you
mode"both", "screen", "camera" or "audio"
videoTitleGeneratedTitle saved with the video
folderId, teamIdWhere the video is saved. folderId wins when both are set
recordingQuality"1080p""720p", "1080p" or "2k"
autoUploadtrueUpload as soon as recording stops. Set false to review first
maxDurationSecondsNo limitStops the recording automatically at this length
hooksFunctions that can approve or block actions (see below)
hookTimeoutMs10000How 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.

The recorder moves through these states, which you can read at any time with recorder.getState():

StateMeaning
idle, initializingBeing set up
readyWaiting for start(). Also the state after a cancel
recording, pausedCapturing
stopping, uploadingFinishing the file and uploading it
completedThe video is uploaded. getResult() returns the DadanVideo
errorSomething failed. A partial take may still be available
destroyeddestroy() was called

Drive your interface from events rather than polling the state.

typescript
const 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, autoplay and playsinline attributes 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) and getDuration() (elapsed milliseconds).

Set autoUpload: false and the recorder keeps the file after stop(). Show it, then upload when the person confirms:

typescript
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.

typescript
const { 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`);
});
  • setMicrophoneEnabled and setCameraEnabled work before start(). 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, and getPermissions() gives a best-effort view of camera and microphone permissions.

Hooks run before an action and can stop it. Return false, or reject, to block the action:

typescript
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,
  },
});
HookRuns before
beforeStartstart(). In screen and both modes it runs after the screen picker, because Safari requires the picker to open straight from the click
beforeStopstop(). Skipped when recording stops because the person ended screen sharing
beforeCancelcancel()
beforeUploadEvery 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.

With maxDurationSeconds, the recorder emits recording.limitReached and stops on its own when the limit is reached. Change the limit later with recorder.setMaxDuration(seconds).

Subscribe with recorder.on(name, handler), which returns a function that removes the listener. once and off are also available.

EventPayload and meaning
readyThe recorder is set up
preview.ready{ stream } for a live preview
recording.started, recording.paused, recording.resumed, recording.stopped{ recordingId, elapsedMs, timestamp }
recording.limitReachedThe 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.trackEndedA 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.unmutedMicrophone state changed
microphone.silenceDetected, microphone.inputRestoredNo sound for a while, then sound again
camera.enabled, camera.disabledCamera 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.failedUpload lifecycle. upload.progress carries { percent, bytesUploaded, totalBytes }
  • Uploads are resumable and retried automatically. upload.retrying tells you when.
  • If an upload fails, stop() rejects with a DadanSdkError and the take stays available. Call retryUpload() to try again.
  • cancelUpload(), cancel() and destroy() 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.

Call 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.

For 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.

Ask AI about this article

What should the AI help you with?

Are you still facing issues?

Reach out to our support team for further assistance.

dadan