npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vidtreo/recorder

v1.8.1

Published

Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Zi

Downloads

1,852

Readme

@vidtreo/recorder

Vidtreo SDK for browser-based video recording and transcoding. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications. Features include camera and screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads.

Installation

npm install @vidtreo/recorder

Direct Upload Transport

Starting with @vidtreo/recorder 1.8.0, video bytes upload directly from the browser to Vidtreo-managed R2 storage through a resumable multipart session. The part size scales with the file (calculateDirectUploadPartSize: about 64 parts, whole MiB, clamped to 5–64 MiB) and the backend returns the authoritative geometry plus the parts already verified by R2. The SDK uploads up to four missing parts at once, computes a SHA-256 manifest checksum plus an R2-supported MD5 transport checksum, obtains a short-lived URL whose Signature V4 headers bind Content-MD5 to that part, and confirms only after all parts succeed.

  • The API key is sent only to the configured Vidtreo backend and is never sent to R2.
  • Upload progress reaches 100% only after the backend accepts confirmation.
  • onProgress is a phased, eased value: preparation and initiation fill 0–5%, transferred bytes fill 5–90%, confirmation eases from 90% toward 99%. It never moves backwards. onProgressDetails keeps the raw loadedBytes/totalBytes and reports the current phase (UploadProgressPhase: "preparing" | "transferring" | "finalizing" | "completed").
  • Each part is retried up to five times with exponential backoff on network errors, inactivity, and HTTP 408/429/5xx. Retries pause while the browser is offline, and an expired part URL is re-signed once.
  • Pending uploads keep one idempotency identity across offline recovery and retries.
  • Retries resume provider-verified parts and never store multipart URLs or checksums in IndexedDB.
  • Aggregate progress remains monotonic across parts and reaches 100% only after confirmation.
  • If confirmation is retried after the backend already accepted the upload, the SDK returns the existing video without uploading the Blob again.
  • Signed upload URLs remain in memory for one attempt and are not stored in IndexedDB or included in SDK errors.

Persistent Upload Queue and Resume

Recordings and native-camera uploads are written to an IndexedDB upload queue before they are sent. If the page is reloaded, the tab crashes, or the network drops, the next recorder instance on the same origin picks the upload up again:

  • The queued upload keeps its idempotency key, so the backend returns the same video and the SDK uploads only the parts R2 has not verified yet.
  • Failed attempts are retried by the queue with exponential backoff (up to 10 attempts) on top of the per-part retries. Retries wait while the browser is offline.
  • Uploads authenticated with apiKey store the key with the queued item. Uploads authenticated with getAccessToken never persist a token: the provider is called again when the upload resumes, so the page must create the recorder (or native-camera handler) with the same getAccessToken after a reload.
  • Errors that cannot succeed on retry (such as upload.insufficient-credits) are reported immediately and are not retried.
  • When IndexedDB is unavailable (private browsing, blocked storage) the SDK uploads directly without persistence.

Version 1.8.0 does not fall back to the legacy Worker-proxied upload endpoint. Applications that need the previous transport must remain on an earlier SDK version while the backend continues to support those clients. Custom Vidtreo backend deployments must expose initiation, part authorization, and confirmation endpoints. Managed R2 CORS must allow browser PUT plus the signed Content-MD5 header and expose ETag.

Quick Start

import { VidtreoRecorder } from '@vidtreo/recorder';

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://core.vidtreo.com', // Optional, defaults to https://core.vidtreo.com
  enableSourceSwitching: true,
  maxRecordingTime: 300000,
  onUploadComplete: (result) => {
    console.log('Recording ID:', result.recordingId);
    console.log('Video URL:', result.uploadUrl);
  },
});

await recorder.startPreview('camera');
await recorder.startRecording({}, 'camera');
const result = await recorder.stopRecording();
recorder.cleanup();

API Reference

VidtreoRecorder

The main class for recording video from camera or screen sources.

Constructor

new VidtreoRecorder(config: VidtreoRecorderConfig)

Creates a new recorder instance with the specified configuration.

Parameters:

  • config.apiKey (required unless getAccessToken is set): Your API key for backend authentication
  • config.getAccessToken (optional): () => Promise<string> that returns a short-lived desktop access token. Use it instead of apiKey when your server mints temporary tokens, so no long-lived key is exposed in the page. The SDK calls it whenever it needs a fresh token, including when a queued upload resumes after a reload. Provide exactly one of apiKey or getAccessToken
  • config.apiUrl (optional): Your backend API URL endpoint. Defaults to https://core.vidtreo.com if not provided
  • config.enableSourceSwitching (optional): Enable switching between camera and screen during recording. Default: false
  • config.enableMute (optional): Enable mute/unmute functionality. When disabled, muteAudio(), unmuteAudio(), and toggleMute() will throw errors. Default: true
  • config.enablePause (optional): Enable pause/resume functionality. When disabled, pauseRecording() and resumeRecording() will throw errors. Default: true
  • config.enableDeviceChange (optional): Enable device selection functionality. When disabled, getAvailableDevices() will throw an error. Default: true
  • config.maxRecordingTime (optional): Maximum recording duration in milliseconds. When the maximum time is reached, recording automatically stops. If not set, recording can continue indefinitely until manually stopped. Examples: 300000 (5 minutes), 600000 (10 minutes), 1800000 (30 minutes)
  • config.countdownDuration (optional): Countdown duration in milliseconds before recording starts. Default: 0 (no countdown)
  • config.userMetadata (optional): Custom metadata object to include with uploads
  • config.onUploadComplete (optional): Callback invoked when upload completes successfully. Receives a RecordingUploadResult
  • config.onUploadProgress (optional): Callback invoked during upload with progress value (0-1)
  • config.onUploadError (optional): Callback invoked when upload fails. Read the error code with getUploadErrorCode
  • config.onAudioWarning (optional): Callback invoked when an audio issue is detected during recording. Receives an AudioWarning object. See Audio Warnings for details
  • config.onRecordingStart (optional): Callback invoked when recording starts
  • config.onRecordingStop (optional): Callback invoked when recording stops
  • config.onError (optional): Callback invoked when a stream error occurs

Throws: Error if neither apiKey nor getAccessToken is provided. If both are provided it throws the shared configuration error "Provide either apiKey or getAccessToken, not both" with code: CONFIG_ERROR_CREDENTIAL_CONFLICT ("config.credential-conflict"). The same error is thrown by RecorderController.initialize, ConfigManager, ConfigService, NativeCameraHandler and VideoUploadService, and by the React, Vue and Web Component packages. Use isCredentialConflictError(error), hasCredentialConflict(apiKey, getAccessToken) or assertSingleCredential(apiKey, getAccessToken) to check it yourself.

Methods

initialize(): Promise<void>

Initializes the recorder with the provided configuration. Called automatically when needed, but can be called explicitly for early initialization.

Returns: Promise<void>

startPreview(sourceType?: SourceType): Promise<MediaStream>

Starts a preview stream from the specified source without recording.

Parameters:

  • sourceType (optional): Source type to preview. Either 'camera' or 'screen'. Default: 'camera'

Returns: Promise<MediaStream> - The preview media stream

Throws: Error if stream initialization fails

startRecording(options?: RecordingStartOptions, sourceType?: SourceType): Promise<void>

Starts recording from the specified source. If no preview is active, automatically starts one.

Parameters:

  • options (optional): Recording options object
    • options.video: Video constraints (boolean or CameraConstraints object)
    • options.audio: Audio constraints (boolean or MediaTrackConstraints object)
  • sourceType (optional): Source type to record from. Either 'camera' or 'screen'. Default: 'camera'

Returns: Promise<void>

Throws: Error if recording cannot be started

stopRecording(): Promise<RecordingStopResult>

Stops the current recording, transcodes the video, and uploads it to the backend.

Returns: Promise<RecordingStopResult> - Object containing:

  • recordingId: Unique identifier for the uploaded recording
  • uploadUrl: URL where the video can be accessed
  • blob: The recorded video as a Blob

Throws: Error if recording is not active or upload fails

switchSource(sourceType: SourceType): Promise<void>

Switches the recording source between camera and screen during an active recording.

Parameters:

  • sourceType: Target source type. Either 'camera' or 'screen'

Returns: Promise<void>

Throws: Error if source switching is not enabled or if not currently recording

reselectScreenSource(): Promise<boolean>

Opens the screen picker again while the source is 'screen' or 'camera-screen' and swaps the newly selected screen or window into the running preview or recording. The source type, the microphone and the camera bubble are kept.

Returns: Promise<boolean> - true when a new screen was applied, false when the picker was cancelled

Throws: Error if source switching is not enabled or no screen capture is active

getAvailableDevices(): Promise<AvailableDevices>

Retrieves the list of available camera and microphone devices.

Returns: Promise<AvailableDevices> - Object containing:

  • video: Array of available video input devices
  • audio: Array of available audio input devices

Throws: Error if device change functionality is disabled (enableDeviceChange === false)

muteAudio(): void

Mutes the audio track during recording.

Throws: Error if mute functionality is disabled (enableMute === false)

unmuteAudio(): void

Unmutes the audio track during recording.

Throws: Error if mute functionality is disabled (enableMute === false)

toggleMute(): void

Toggles the mute state of the audio track.

Throws: Error if mute functionality is disabled (enableMute === false)

isMuted(): boolean

Returns the current mute state.

Returns: true if audio is muted, false otherwise

pauseRecording(): void

Pauses the current recording. Video frames are not captured while paused.

Throws: Error if pause functionality is disabled (enablePause === false)

resumeRecording(): void

Resumes a paused recording.

Throws: Error if pause functionality is disabled (enablePause === false)

isPaused(): boolean

Returns the current pause state.

Returns: true if recording is paused, false otherwise

getRecordingState(): RecordingState

Returns the current recording state.

Returns: One of:

  • 'idle': Not recording
  • 'countdown': Countdown in progress before recording
  • 'recording': Actively recording
getStream(): MediaStream | null

Returns the current media stream, or null if no stream is active.

Returns: MediaStream | null

cleanup(): void

Cleans up all resources, stops active streams, and cancels any pending operations. Should be called when the recorder is no longer needed.

Type Definitions

VidtreoRecorderConfig

interface VidtreoRecorderConfig {
  apiKey?: string;
  getAccessToken?: () => Promise<string>;
  apiUrl?: string;
  enableSourceSwitching?: boolean;
  enableMute?: boolean;
  enablePause?: boolean;
  enableDeviceChange?: boolean;
  maxRecordingTime?: number;
  countdownDuration?: number;
  userMetadata?: Record<string, unknown>;
  enableTabVisibilityOverlay?: boolean;
  tabVisibilityOverlayText?: string;
  captureSystemAudio?: boolean;
  onUploadComplete?: (result: RecordingUploadResult) => void;
  onUploadProgress?: (progress: number) => void;
  onUploadError?: (error: Error) => void;
  onRecordingStart?: () => void;
  onRecordingStop?: () => void;
  onAudioWarning?: (warning: AudioWarning) => void;
  onError?: (error: Error) => void;
}

enableTabVisibilityOverlay applies to camera recordings. When enabled and tabVisibilityOverlayText is omitted or blank, the SDK uses "User in another tab" as a default fallback. Overlay rendering is skipped for screen capture recordings.

Note: For web component usage, see the @vidtreo/recorder-wc package. Use the max-recording-time attribute instead of maxRecordingTime. The attribute accepts a numeric value in milliseconds as a string (e.g., max-recording-time="300000" for 5 minutes).

RecordingStartOptions

interface RecordingStartOptions {
  video?: boolean | CameraConstraints;
  audio?: boolean | MediaTrackConstraints;
}

RecordingUploadResult

Payload passed to every upload-complete callback (VidtreoRecorder onUploadComplete, UploadCallbacks.onSuccess, the upload queue, native-camera uploads, and the React/Vue/Web Component packages). It contains every field of the backend VideoUploadResult plus the backward-compatible recordingId and uploadUrl fields.

type RecordingUploadResult = {
  recordingId: string;           // same value as id
  uploadUrl: string;             // "" when the backend has no URL yet
  id: string;
  publicId: string;
  filename: string;
  fileSize: number;
  mimeType: string;
  duration: number | null;
  status: string;
  createdAt: string;
  transferStatus: "pending" | "transferring" | "transferred" | "failed" | null;
  transferError: string | null;
  transferredAt: string | null;
  r2DeleteAfter: string | null;
  r2DeletedAt: string | null;
};

Use createRecordingUploadResult(videoUploadResult) to build this payload from a raw VideoUploadService result.

RecordingStopResult

interface RecordingStopResult {
  recordingId: string;
  uploadUrl: string;
  blob: Blob;
}

SourceType

type SourceType = 'camera' | 'screen' | 'camera-screen';

'camera-screen' is opt-in and only used when passed explicitly. See Screen-only capture and camera + screen.

RecordingState

type RecordingState = 'idle' | 'countdown' | 'recording';

AvailableDevices

interface AvailableDevices {
  video: MediaDeviceInfo[];
  audio: MediaDeviceInfo[];
}

AudioWarning

type AudioWarning =
  | { code: 'audio.no-signal'; durationMs: number }
  | { code: 'audio.no-chunks'; durationMs: number }
  | { code: 'audio.low-signal'; peak: number; rms: number }
  | { code: 'audio.track-ended' }
  | { code: 'audio.track-muted-by-browser' };

CameraConstraints

interface CameraConstraints {
  width?: number | { ideal?: number; min?: number; max?: number };
  height?: number | { ideal?: number; min?: number; max?: number };
  frameRate?: number | { ideal?: number; min?: number; max?: number };
}

User-Facing Messages

The SDK emits structured events for common issues. Your app should listen and display appropriate UX for each.

Audio Warnings

Set onAudioWarning in the constructor config. Each warning fires once per recording session (deduped by code). Use these to show real-time feedback to the user.

| Code | Meaning | Suggested UX | |------|---------|-------------| | audio.no-signal | Mic is producing silence for an extended period. durationMs indicates how long. | "Microphone not detected. Check your mic is plugged in and selected." | | audio.no-chunks | No audio data received from the microphone for an extended period. durationMs indicates how long. | "No microphone input. Check microphone permissions and connection." | | audio.low-signal | Audio level is very low. peak and rms contain measured levels (0–1 range). | "Audio is very quiet. Speak louder or move closer to the microphone." | | audio.track-ended | The audio track ended unexpectedly (device disconnected or revoked). | "Microphone disconnected. Reconnect and restart recording." | | audio.track-muted-by-browser | Browser muted the track automatically (e.g., browser-level mute or policy). | "Microphone muted by browser. Check browser mic settings." |

Example:

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  onAudioWarning: (warning) => {
    switch (warning.code) {
      case 'audio.no-signal':
      case 'audio.no-chunks':
        showToast('No microphone input detected. Check your mic.');
        break;
      case 'audio.low-signal':
        showToast('Audio is very quiet. Speak louder or check your mic.');
        break;
      case 'audio.track-ended':
        showToast('Microphone disconnected. Reconnect and try again.');
        break;
      case 'audio.track-muted-by-browser':
        showToast('Microphone muted by browser. Check browser settings.');
        break;
    }
  },
});

No Video Signal

The SDK emits an error via onError when no video frames arrive from the camera:

  • Error message contains video-first-frame-timeout
  • Cause: camera in use by another app, camera permission revoked, or hardware failure
  • Suggested UX: "Camera not responding. Close other apps using the camera and try again."
const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  onError: (error) => {
    if (error.message.includes('video-first-frame-timeout')) {
      showToast('Camera not responding. Close other apps using the camera.');
    }
  },
});

Storage Restricted

Storage issues surface in two ways:

  1. Quota exceeded during upload queue write: error message contains Storage quota exceeded. Suggested UX: "Device storage is full. Free up space to save recordings."
  2. Proactive quota check via QuotaManager (exported from this package):
import { QuotaManager } from '@vidtreo/recorder';

const quotaManager = new QuotaManager();
const quota = await quotaManager.getQuota();

if (await quotaManager.shouldWarn()) {
  showToast(`Storage usage at ${Math.round(quota.percentage)}%. Free up space.`);
}

if (await quotaManager.isCritical()) {
  showToast('Storage critically low. Recording may fail to save.');
}
  • shouldWarn() triggers at 80% usage (configurable threshold)
  • isCritical() triggers at 95% usage (configurable threshold)

Unsupported Browser

Use validateBrowserSupport() before initializing the recorder:

import { validateBrowserSupport } from '@vidtreo/recorder';

try {
  validateBrowserSupport();
} catch (error) {
  // error.code === 'browser.unsupported'
  // error.browserName — detected browser name
  // error.browserVersion — detected version
  // error.missingCapabilities — list of missing APIs
  // error.resolutionStage — 'policy' (known unsupported) or 'feature-preflight' (missing API)
  showToast('Your browser is not supported. Use Chrome, Edge, or Safari 26+.');
}

For richer error text, use getBrowserErrorText() and getCameraErrorText() from @vidtreo/recorder with your own translations (see exported types AudioErrorTranslations, BrowserErrorLinkContent).

Usage Examples

Basic Recording Workflow

import { VidtreoRecorder } from '@vidtreo/recorder';

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com', // Optional, defaults to https://core.vidtreo.com
  onUploadComplete: (result) => {
    console.log('Uploaded:', result.uploadUrl);
  },
});

try {
  await recorder.startPreview('camera');
  await recorder.startRecording({}, 'camera');

  setTimeout(async () => {
    const result = await recorder.stopRecording();
    console.log('Recording ID:', result.recordingId);
    recorder.cleanup();
  }, 10000);
} catch (error) {
  console.error('Recording failed:', error);
  recorder.cleanup();
}

Recording with Maximum Time Limit

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  maxRecordingTime: 300000,
  onRecordingStop: () => {
    console.log('Recording stopped automatically after 5 minutes');
  },
});

await recorder.startRecording({}, 'camera');

Recording with Source Switching

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  enableSourceSwitching: true,
  maxRecordingTime: 600000,
});

await recorder.startRecording({}, 'camera');

setTimeout(async () => {
  await recorder.switchSource('screen');
}, 5000);

setTimeout(async () => {
  await recorder.switchSource('camera');
}, 10000);

setTimeout(async () => {
  const result = await recorder.stopRecording();
  recorder.cleanup();
}, 15000);

Note: When maxRecordingTime is set, the recording will automatically stop when the time limit is reached, even if the timeout callbacks are still pending. The onRecordingStop callback will be invoked when the maximum time is reached.

Recording with Mute Control

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
});

await recorder.startRecording({}, 'camera');

recorder.muteAudio();
setTimeout(() => recorder.unmuteAudio(), 5000);
setTimeout(() => recorder.toggleMute(), 10000);

setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 15000);

Recording with Pause and Resume

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
});

await recorder.startRecording({}, 'camera');

setTimeout(() => recorder.pauseRecording(), 3000);
setTimeout(() => recorder.resumeRecording(), 6000);

setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 10000);

Recording with Upload Progress

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  onUploadProgress: (progress) => {
    const percentage = Math.round(progress * 100);
    console.log(`Upload: ${percentage}%`);
  },
  onUploadComplete: (result) => {
    console.log('Upload complete:', result.uploadUrl);
  },
  onUploadError: (error) => {
    console.error('Upload failed:', error.message);
  },
});

await recorder.startRecording({}, 'camera');
setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 5000);

Recording with Custom Metadata

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  userMetadata: {
    userId: 'user-123',
    sessionId: 'session-456',
    projectId: 'project-789',
  },
});

await recorder.startRecording({}, 'camera');
setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 5000);

Recording with Countdown

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  countdownDuration: 3000,
});

await recorder.startPreview('camera');
await recorder.startRecording({}, 'camera');
setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 10000);

Device Selection

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
});

const devices = await recorder.getAvailableDevices();
console.log('Cameras:', devices.video.map(d => d.label));
console.log('Microphones:', devices.audio.map(d => d.label));

await recorder.startRecording({}, 'camera');
setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 5000);

Recording State Monitoring

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  onRecordingStart: () => {
    console.log('Recording started');
  },
  onRecordingStop: () => {
    console.log('Recording stopped');
  },
});

await recorder.startRecording({}, 'camera');

const checkState = setInterval(() => {
  const state = recorder.getRecordingState();
  const isPaused = recorder.isPaused();
  const isMuted = recorder.isMuted();
  console.log(`State: ${state}, Paused: ${isPaused}, Muted: ${isMuted}`);
}, 1000);

setTimeout(async () => {
  clearInterval(checkState);
  await recorder.stopRecording();
  recorder.cleanup();
}, 10000);

Screen Recording

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  enableSourceSwitching: true,
});

await recorder.startPreview('screen');
await recorder.startRecording({}, 'screen');
setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 30000);

Error Handling

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  onError: (error) => {
    console.error('Stream error:', error.message);
  },
  onUploadError: (error) => {
    console.error('Upload error:', error.message);
  },
});

try {
  await recorder.startRecording({}, 'camera');
  const result = await recorder.stopRecording();
  console.log('Success:', result.uploadUrl);
} catch (error) {
  console.error('Recording error:', error);
} finally {
  recorder.cleanup();
}

Upload Errors

Upload failures that need user-facing handling carry a stable code. Read it with getUploadErrorCode:

import {
  getUploadErrorCode,
  UPLOAD_ERROR_INSUFFICIENT_CREDITS,
  type UploadErrorCode,
} from '@vidtreo/recorder';

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  onUploadError: (error) => {
    const code: UploadErrorCode | null = getUploadErrorCode(error);
    if (code === UPLOAD_ERROR_INSUFFICIENT_CREDITS) {
      showMessage('This video could not be uploaded. Please contact the site administrator.');
    }
  },
});

| Code | Meaning | |------|---------| | upload.insufficient-credits | The backend rejected the upload with HTTP 402 because the account has no credits left. The upload is not retried. |

getUploadErrorCode returns null for every other error (network failures, timeouts, and so on). Those errors are retried automatically before they are reported.

Recording with Disabled Features

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com',
  enableMute: false,
  enablePause: false,
  enableSourceSwitching: false,
  enableDeviceChange: false,
});

await recorder.startRecording({}, 'camera');

try {
  recorder.muteAudio();
} catch (error) {
  console.error('Mute is disabled:', error.message);
}

try {
  recorder.pauseRecording();
} catch (error) {
  console.error('Pause is disabled:', error.message);
}

setTimeout(async () => {
  await recorder.stopRecording();
  recorder.cleanup();
}, 10000);

Web Component Usage

The web component is available in a separate package: @vidtreo/recorder-wc

For web component installation, usage, and examples, please refer to the @vidtreo/recorder-wc documentation.

Advanced Usage

Screen-only capture and camera + screen (advanced)

These capabilities are opt-in. Without the options below the recorder behaves exactly as before: screen capture starts from the camera preview and 'camera-screen' is never used.

Screen-only capture

Set startScreenWithoutCamera: true to start startPreview('screen') / startRecording({}, 'screen') with getDisplayMedia for video and a microphone-only getUserMedia request. The camera is never opened. Mute, audio levels, microphone changes and stopping work as with the camera. Switching to 'camera' or 'camera-screen' opens the camera at that moment. If the user cancels the screen picker, the start rejects with an error whose code is ERROR_SCREEN_SELECTION_CANCELLED ('screen.selection-cancelled').

Camera + screen ('camera-screen')

'camera-screen' records the screen as the full frame with the camera as a picture-in-picture bubble. Preview and recording use the same composed video track; audio is the microphone.

import { isPictureInPictureSupported, VidtreoRecorder } from '@vidtreo/recorder';

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  enableSourceSwitching: true,
  startScreenWithoutCamera: true,
  pictureInPicture: { position: 'bottom-right', shape: 'circle', size: 0.22 },
});

if (isPictureInPictureSupported()) {
  await recorder.startRecording({}, 'camera-screen');
  await recorder.switchSource('screen');
  await recorder.switchSource('camera-screen');
  const cameraOnly = recorder.getCameraStream();
}

pictureInPicture options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' \| { x: number; y: number } | 'bottom-right' | Bubble corner, or the normalized top-left of the bubble in the output frame (0 to 1 each) | | shape | 'circle' \| 'rounded' | 'circle' | Bubble shape | | size | number (0.05 to 0.5) | 0.22 | Bubble diameter as a fraction of the frame height |

Invalid values throw during initialization. The bubble has a 3% margin, a white border of about 2% of its diameter and a soft shadow. The camera image is center-cropped into the bubble and is not mirrored.

  • Switching between 'camera', 'screen' and 'camera-screen' works in preview and while recording, without stopping the recording.
  • If the user stops sharing from the browser or OS, the recorder switches to the camera and keeps recording.
  • Stopping, stopPreview and cleanup() stop the screen, camera and microphone tracks and the compositor.
  • getCameraStream() returns a video-only stream with the live camera track (in 'camera' and 'camera-screen'), or null.
  • Composition runs in a dedicated Web Worker (MediaStreamTrackProcessor → OffscreenCanvas → MediaStreamTrackGenerator). New frames drive it, not timers or requestAnimationFrame, so it keeps running while the page is hidden or minimized. Output resolution is the screen resolution capped to the recording resolution. Frame rate follows the screen capture, capped at 30 fps.
  • 'camera-screen' requires MediaStreamTrackProcessor, MediaStreamTrackGenerator and OffscreenCanvas (Chromium-based browsers and Electron). Check with isPictureInPictureSupported(). On other browsers the switch reports an error with code ERROR_PICTURE_IN_PICTURE_UNSUPPORTED ('screen.picture-in-picture-unsupported') and the current source stays active.

Free position and live layout updates

A free position places the bubble's top-left corner at { x, y } as fractions of the output frame width and height. The point is clamped so the whole bubble stays inside the frame for its size and the frame aspect ratio; corner presets keep their 3% margin, free positions have none.

setPictureInPicture(update) merges a partial update into the current configuration, validates it and applies it to the running compositor without restarting it, in preview and while recording. The next composed frame eases toward the new rect over about 120 ms; pass smooth: false to jump. Without a running compositor the update is stored and used the next time 'camera-screen' starts. Invalid values throw and keep the previous layout.

recorder.setPictureInPicture({ position: { x: 0.72, y: 0.64 }, size: 0.25 });
recorder.setPictureInPicture({ position: 'top-left', smooth: false });
const layout = recorder.getPictureInPictureConfig();

RecorderController exposes the same setPictureInPicture(update) and getPictureInPictureConfig().

The React hook useVidtreoRecorder accepts the same startScreenWithoutCamera and pictureInPicture options (pictureInPicture is the initial value) and exposes actions.setPictureInPicture(update). It exposes state.sourceType and state.cameraStream. The prebuilt UI components do not offer these sources.

System audio (advanced)

Set captureSystemAudio: true (also accepted by useVidtreoRecorder and RecorderController.configureSources) to record the computer's sound (calls, videos, other apps) together with the microphone when recording 'screen' or 'camera-screen'. Default false: the display stream's audio is requested with audio: false and any audio track is stopped, exactly as before.

  • Screen pickers are requested with audio: true. Whether audio comes back depends on the browser and the user's choice (Chrome only offers it for tabs, or for the whole screen on Windows/ChromeOS; Electron apps return it from setDisplayMediaRequestHandler with audio: 'loopback'). Without a display audio track the recording falls back to the microphone, without an error.
  • While recording, the microphone and the system audio are mixed into one audio track with Web Audio (AudioContext → one MediaStreamAudioSourceNode and GainNode per input → MediaStreamAudioDestinationNode). Web Audio resamples both inputs to the context rate. The context is created when the recording starts and closed when it stops.
  • Mute only silences the microphone; the system audio keeps recording. Audio levels (getAudioStreamForAnalysis) keep showing the microphone only.
  • Reselecting the screen, or switching between camera and screen sources, swaps the system audio input without stopping the recording. A recording started from the camera picks up system audio once it switches to a screen source.
  • The preview never plays the system audio. If the microphone picks up the speakers, the system audio is recorded twice (once directly, once through the room). Browser echo cancellation (echoCancellation, on by default) only cancels audio the page itself plays, so recommend headphones during calls.

Recording title

Call setRecordingTitle(title) before or during a recording to name the uploaded file <title>.<ext> instead of recording-<timestamp>.<ext>. The title is trimmed, whitespace is collapsed, control characters and / \ : * ? " < > | are removed, leading dots are stripped and the result is capped at 120 characters. An empty result keeps the default name. The title is cleared once the recording's upload is queued, so set it again for the next recording. Pass null to clear it.

recorder.setRecordingTitle('Demo – Q4 ventas');
await recorder.startRecording({}, 'camera');
const result = await recorder.stopRecording(); // uploads "Demo – Q4 ventas.mp4"

The same method exists on RecorderController, the React hook exposes it as actions.setRecordingTitle(title), and sanitizeRecordingTitle(title) is exported to preview the sanitized name.

Telemetry

The recorder automatically sends telemetry events to monitor SDK usage, detect issues, and improve the product. Telemetry is always active and cannot be disabled through configuration.

When Telemetry is Activated

Telemetry is initialized when you call initialize() on the recorder instance:

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://api.example.com', // This URL is also used for telemetry
});

await recorder.initialize(); // Telemetry client is initialized here

Telemetry Endpoint

Telemetry events are sent to:

  • Default: {apiUrl}/api/v1/telemetry
  • Same apiUrl used for config and upload operations

The apiUrl can be overridden when creating the recorder:

const recorder = new VidtreoRecorder({
  apiKey: 'your-api-key',
  apiUrl: 'https://custom-api.example.com', // Telemetry, config, and upload all use this URL
});

Delivery Behavior

Telemetry delivery is optimized for low overhead and no duplicate events:

  • Events are batched in memory and flushed after 10 events or 1 second
  • One-time events are deduped per session (sdk.init.started, sdk.init.succeeded, sdk.init.failed)
  • Noisy errors are throttled (currently stream.error emits at most once per 5 seconds)

Events Tracked

The SDK automatically tracks the following telemetry events:

| Event Name | Category | Description | |------------|----------|-------------| | Lifecycle Events | | | | sdk.init.started | lifecycle | Sent when recorder initialization begins | | sdk.init.succeeded | lifecycle | Sent when recorder initialization completes successfully | | sdk.init.failed | error | Sent when recorder initialization fails (includes error details) | | Preview Events | | | | preview.start.succeeded | interaction | Sent when preview stream starts successfully | | preview.start.failed | error | Sent when preview stream fails to start (includes error details) | | Recording Events | | | | recording.start.requested | interaction | Sent when user requests to start recording | | recording.start.succeeded | interaction | Sent when recording starts successfully | | recording.start.failed | error | Sent when recording fails to start (includes error details) | | recording.stop.requested | interaction | Sent when user requests to stop recording | | recording.stop.succeeded | interaction | Sent when recording stops successfully | | recording.stop.failed | error | Sent when recording fails to stop (includes error details) | | Upload Events | | | | upload.started | performance | Sent when video upload begins | | upload.succeeded | performance | Sent when video upload completes successfully | | upload.failed | error | Sent when video upload fails (includes error details) | | Source Switch Events | | | | source.switch.requested | interaction | Sent when source switch is requested | | source.switch.succeeded | interaction | Sent when source switch completes successfully | | source.switch.failed | error | Sent when source switch fails (includes error details) | | Stream Events | | | | stream.error | error | Sent when a stream error occurs (includes error details) |

Event Categories

Events are categorized for analysis:

  • lifecycle: SDK initialization events
  • interaction: User-initiated actions (start/stop/switch)
  • performance: Upload performance metrics
  • error: Any error that occurs during operations

Event Properties

Events include the following properties automatically:

  • browserName: Detected browser (chrome, firefox, safari, edge, unknown)
  • sourceType: Source being used (camera or screen)
  • filename: Name of uploaded file (upload events)
  • duration: Video duration in milliseconds (upload events)
  • recordingId: ID of uploaded recording (upload.succeeded)

Error Details

Error events include additional information:

  • error.message: Human-readable error message
  • error.name: Error type/name
  • error.stack: Stack trace (if available)

Telemetry Payload

Each telemetry request includes:

{
  events: [
    {
      event: string,               // Event name
      category: string,            // lifecycle | interaction | performance | error
      timestamp: number,           // Unix timestamp in milliseconds
      installationId: string,      // Persistent per-browser installation ID
      sdkVersion: string,          // SDK version from package.json
      fingerprint: {
        userAgent?: string,
        language?: string,
        platform?: string,
        hardwareConcurrency?: number,
        deviceMemory?: number,
      },
      context?: {
        sessionId?: string,
        userId?: string,
        environmentId?: string,
        appVersion?: string,
        release?: string,
        pageUrl?: string,
        referrerUrl?: string,
        sdkLocation?: string,
        clientLocation?: string,
      },
      properties?: Record<string, unknown>,
      error?: {
        message: string,
        name?: string,
        stack?: string,
      },
    },
  ];
}

Note: The API key is only sent in the Authorization header for other requests (like upload). It is not included in telemetry payloads.

Installation ID

A unique installationId is generated per browser and persisted in localStorage under key VIDTREO_INSTALLATION_ID. This ID persists across sessions and helps identify unique installations.

Privacy

  • API keys are sent in the Authorization header as Bearer ${apiKey} (not hashed)
  • Installation IDs are anonymous and randomly generated
  • User and session identifiers are optional and provided by your application
  • No personally identifiable information is collected unless explicitly provided through userMetadata

Low-Level APIs

For advanced use cases, the package exports lower-level APIs that provide more granular control:

Transcoding

import { transcodeVideo, DEFAULT_TRANSCODE_CONFIG } from '@vidtreo/recorder';

const videoBlob = new Blob([videoData], { type: 'video/mp4' });
const result = await transcodeVideo(videoBlob, {
  width: 1920,
  height: 1080,
  fps: 60,
  bitrate: 2000000,
});

console.log('Transcoded size:', result.buffer.byteLength);

RecorderController

For fine-grained control over the recording process:

import { RecorderController } from '@vidtreo/recorder';

const controller = new RecorderController({
  recording: {
    onStateChange: (state) => console.log('State:', state),
  },
});

await controller.initialize({
  apiKey: 'your-api-key',
});

Native Camera Uploads

NativeCameraHandler validates a video picked with the device camera or gallery (<input type="file" capture>), transcodes it, and uploads it through the same persistent queue as recorded videos. UI packages use this single API for their native-camera mode.

import {
  ConfigService,
  NativeCameraHandler,
  VideoUploadService,
  getUploadErrorCode,
} from '@vidtreo/recorder';

const handler = new NativeCameraHandler(
  {
    apiKey: 'your-api-key',            // or getAccessToken: () => fetchToken()
    backendUrl: 'https://core.vidtreo.com',
    maxRecordingTime: 300000,
    maxFileSize: 500 * 1024 * 1024,
    userMetadata: { interviewId: '42' },
  },
  ConfigService.getInstance({ apiKey: 'your-api-key', backendUrl: 'https://core.vidtreo.com' }),
  new VideoUploadService()
);

await handler.startUploadQueue({
  onUploadProgress: (progress) => updateProgress(progress),
  onUploadProgressDetails: (details) => updatePhase(details.phase),
  onUploadRetry: (details) => showRetry(details.attempt),
  onUploadComplete: (result) => saveVideo(result),
  onUploadError: (error) => showError(getUploadErrorCode(error)),
});

const file = await handler.handleFileSelection(input.files[0]);
const result = await handler.processAndUpload(
  (transcodeProgress) => updateTranscode(transcodeProgress),
  (uploadProgress) => updateProgress(uploadProgress),
  (details) => updatePhase(details.phase),
  (retry) => showRetry(retry.attempt)
);
console.log(result.recordingId, result.transferStatus, result.blob);

handler.destroy();
  • startUploadQueue(callbacks?) opens the IndexedDB queue and immediately resumes uploads left over from a previous page load (same idempotency key, only missing parts are sent). Its callbacks receive events for those resumed uploads and for queue-level errors. It resolves to false when persistent storage is unavailable; uploads then go directly to the backend.
  • processAndUpload(onTranscodeProgress, onUploadProgress, onUploadProgressDetails?, onUploadRetry?) queues the transcoded file and resolves with a NativeCameraUploadResult (RecordingUploadResult & { blob: Blob }) once the upload is confirmed. It rejects only with a terminal error, after the queue has exhausted its retries or immediately for upload.insufficient-credits. Events for this upload go to the per-call callbacks, not to the startUploadQueue callbacks.
  • Calling processAndUpload without startUploadQueue opens the queue on demand.
  • destroy() stops the queue timers. An upload that is still pending stays in IndexedDB and resumes the next time a handler or recorder starts the queue.

Stream Management

import { CameraStreamManager } from '@vidtreo/recorder';

const streamManager = new CameraStreamManager();
await streamManager.startStream();
const stream = streamManager.getStream();

Configuration

Video transcoding configuration is managed through your backend API. The recorder fetches configuration using the provided apiKey and apiUrl.

Watermark Support

The recorder supports real-time watermark rendering. This is highly optimized for performance:

  • One-time preparation: The watermark is loaded and pre-rendered once before recording starts.
  • Dynamic Scaling: The watermark automatically scales to 7% of the video width while maintaining its aspect ratio.
  • Efficient Composition: The watermark is drawn directly onto a composition canvas, avoiding redundant opacity calculations per frame.

Watermark Configuration:

  • url: The URL of the image (PNG, JPG, or SVG). Data URLs (base64) are also supported and recommended for reliability.
  • opacity: Watermark opacity (0.0 to 1.0). Default: 1.0 (Recommended for best video compression).
  • position: One of "top-left", "top-right", "bottom-left", "bottom-right", or "center". Default: "bottom-right".

Default transcoding settings include:

  • Format: MP4
  • Frame rate: 30 fps
  • Resolution: 1280x720
  • Bitrate: 500 kbps
  • Audio codec: Opus
  • Preset: Medium quality

These defaults are used when backend configuration is unavailable or during initialization.

Browser Compatibility

This package requires modern browser APIs for full functionality. The most critical requirement is support for the WebCodecs API, which enables real-time MP4 transcoding.

Full Support (All Features)

The following browsers support all features including real-time MP4 transcoding:

  • Chrome 94+ - Full support for all features
  • Edge (Chromium) 94+ - Full support, same as Chrome
  • Opera 80+ - Full support, Chromium-based
  • Brave 1.30+ - Full support, Chromium-based
  • Vivaldi 5.0+ - Full support, Chromium-based
  • Safari (macOS/iOS) 26.0+ - Full WebCodecs support

Partial Support (Core Features Only)

The following browsers support core recording features but may have limitations:

  • Safari (macOS/iOS) 16.4-25.x - Partial WebCodecs support; some codecs may not be available

Required Browser APIs

  • WebCodecs API - Required for real-time MP4 transcoding (via mediabunny)
  • Web Workers - Required for video processing (no fallback available)
  • AudioWorklet - Required for audio processing and PCM capture
  • AudioContext - Required for AudioWorklet pipelines
  • MediaStreamTrackProcessor - Required for track processing in workers
  • VideoFrame - Required for video frame processing in workers
  • Screen Capture API (getDisplayMedia) - Required for screen recording
  • MediaDevices API (getUserMedia) - Required for camera/microphone access
  • IndexedDB - Required for persistent upload queue
  • Storage API - Required for storage quota management

Note: This package requires modern browsers with full Web Worker and AudioWorklet support. There is no fallback for browsers without Worker, AudioWorklet, MediaStreamTrackProcessor, VideoFrame, or OffscreenCanvas APIs.

Feature Support by Browser

| Feature | Chrome 94+ | Edge 94+ | Safari 26.0+ | Safari 16.4-25.x | |---------|------------|---------|--------------|------------------| | Camera Recording | ✅ | ✅ | ✅ | ✅ | | Screen Recording | ✅ | ✅ | ✅ | ✅ | | Real-time MP4 Transcoding | ✅ | ✅ | ✅ | ⚠️ (Partial) | | Audio Level Analysis | ✅ | ✅ | ✅ | ✅ | | Source Switching | ✅ | ✅ | ✅ | ✅ | | Device Switching | ✅ | ✅ | ✅ | ✅ | | Persistent Upload Queue | ✅ | ✅ | ✅ | ✅ |

Unsupported Browsers

  • Internet Explorer (all versions)
  • Firefox (all versions)
  • Samsung Internet
  • Android WebView and in-app browsers
  • Google App browser
  • Safari < 14.1 (missing AudioWorklet)
  • Safari < 16.4 (missing OffscreenCanvas and WebCodecs)
  • Edge Legacy (pre-Chromium)
  • Chrome < 94 (missing WebCodecs)

Mobile Browser Support

  • Chrome Android 94+ - Full support
  • Edge Android 94+ - Full support
  • Safari iOS 16.4+ - Partial support (WebCodecs partial)
  • Safari iOS 26.0+ - Full support
  • Samsung Internet - Not supported
  • Android WebView and in-app browsers - Not supported
  • Google App browser - Not supported
  • Firefox Android - Not supported

Important Notes

  1. HTTPS Required: Media capture APIs require HTTPS (or localhost) in most browsers
  2. User Permissions: Camera, microphone, and screen capture require explicit user permission
  3. Web Workers Required: This package requires Web Workers and AudioWorklet for video processing. Browsers without Worker support (or missing MediaStreamTrackProcessor, VideoFrame, or OffscreenCanvas) are not supported
  4. No Fallback: The package does not include a fallback for browsers without Web Worker support. Ensure your target browsers meet the minimum requirements

For detailed browser compatibility information, API requirements, and known limitations, see BROWSER_COMPATIBILITY.md.

License

MIT