@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/recorderDirect 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. onProgressis 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.onProgressDetailskeeps the rawloadedBytes/totalBytesand reports the currentphase(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
apiKeystore the key with the queued item. Uploads authenticated withgetAccessTokennever 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 samegetAccessTokenafter 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 unlessgetAccessTokenis set): Your API key for backend authenticationconfig.getAccessToken(optional):() => Promise<string>that returns a short-lived desktop access token. Use it instead ofapiKeywhen 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 ofapiKeyorgetAccessTokenconfig.apiUrl(optional): Your backend API URL endpoint. Defaults tohttps://core.vidtreo.comif not providedconfig.enableSourceSwitching(optional): Enable switching between camera and screen during recording. Default:falseconfig.enableMute(optional): Enable mute/unmute functionality. When disabled,muteAudio(),unmuteAudio(), andtoggleMute()will throw errors. Default:trueconfig.enablePause(optional): Enable pause/resume functionality. When disabled,pauseRecording()andresumeRecording()will throw errors. Default:trueconfig.enableDeviceChange(optional): Enable device selection functionality. When disabled,getAvailableDevices()will throw an error. Default:trueconfig.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 uploadsconfig.onUploadComplete(optional): Callback invoked when upload completes successfully. Receives aRecordingUploadResultconfig.onUploadProgress(optional): Callback invoked during upload with progress value (0-1)config.onUploadError(optional): Callback invoked when upload fails. Read the error code withgetUploadErrorCodeconfig.onAudioWarning(optional): Callback invoked when an audio issue is detected during recording. Receives anAudioWarningobject. See Audio Warnings for detailsconfig.onRecordingStart(optional): Callback invoked when recording startsconfig.onRecordingStop(optional): Callback invoked when recording stopsconfig.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 objectoptions.video: Video constraints (boolean orCameraConstraintsobject)options.audio: Audio constraints (boolean orMediaTrackConstraintsobject)
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 recordinguploadUrl: URL where the video can be accessedblob: 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 devicesaudio: 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:
- Quota exceeded during upload queue write: error message contains
Storage quota exceeded. Suggested UX: "Device storage is full. Free up space to save recordings." - 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,
stopPreviewandcleanup()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'), ornull.- Composition runs in a dedicated Web Worker (
MediaStreamTrackProcessor→OffscreenCanvas→MediaStreamTrackGenerator). New frames drive it, not timers orrequestAnimationFrame, 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'requiresMediaStreamTrackProcessor,MediaStreamTrackGeneratorandOffscreenCanvas(Chromium-based browsers and Electron). Check withisPictureInPictureSupported(). On other browsers the switch reports an error with codeERROR_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 fromsetDisplayMediaRequestHandlerwithaudio: '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→ oneMediaStreamAudioSourceNodeandGainNodeper 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 hereTelemetry Endpoint
Telemetry events are sent to:
- Default:
{apiUrl}/api/v1/telemetry - Same
apiUrlused 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.erroremits 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
Authorizationheader asBearer ${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 tofalsewhen persistent storage is unavailable; uploads then go directly to the backend.processAndUpload(onTranscodeProgress, onUploadProgress, onUploadProgressDetails?, onUploadRetry?)queues the transcoded file and resolves with aNativeCameraUploadResult(RecordingUploadResult & { blob: Blob }) once the upload is confirmed. It rejects only with a terminal error, after the queue has exhausted its retries or immediately forupload.insufficient-credits. Events for this upload go to the per-call callbacks, not to thestartUploadQueuecallbacks.- Calling
processAndUploadwithoutstartUploadQueueopens 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
- HTTPS Required: Media capture APIs require HTTPS (or localhost) in most browsers
- User Permissions: Camera, microphone, and screen capture require explicit user permission
- 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
- 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
