@vidtreo/recorder
v1.3.3
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
758
Maintainers
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/recorderQuick 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): Your API key for backend authenticationconfig.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 successfullyconfig.onUploadProgress(optional): Callback invoked during upload with progress value (0-1)config.onUploadError(optional): Callback invoked when upload failsconfig.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 apiKey is missing
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
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;
apiUrl?: string;
enableSourceSwitching?: boolean;
enableMute?: boolean;
enablePause?: boolean;
enableDeviceChange?: boolean;
maxRecordingTime?: number;
countdownDuration?: number;
userMetadata?: Record<string, unknown>;
enableTabVisibilityOverlay?: boolean;
tabVisibilityOverlayText?: string;
onUploadComplete?: (result: {
recordingId: string;
uploadUrl: string;
}) => void;
onUploadProgress?: (progress: number) => void;
onUploadError?: (error: Error) => void;
onRecordingStart?: () => void;
onRecordingStop?: () => 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;
}RecordingStopResult
interface RecordingStopResult {
recordingId: string;
uploadUrl: string;
blob: Blob;
}SourceType
type SourceType = 'camera' | 'screen';RecordingState
type RecordingState = 'idle' | 'countdown' | 'recording';AvailableDevices
interface AvailableDevices {
video: MediaDeviceInfo[];
audio: MediaDeviceInfo[];
}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 };
}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();
}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
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',
});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
- Firefox 130+ - Full WebCodecs support
- Safari (macOS/iOS) 26.0+ - Full WebCodecs support
Partial Support (Core Features Only)
The following browsers support core recording features but may have limitations:
- Firefox 76-129 - AudioWorklet supported, but WebCodecs not available. Real-time MP4 transcoding unavailable; falls back to MediaRecorder (WebM format)
- 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+ | Firefox 130+ | Safari 26.0+ | Firefox 76-129 | Safari 16.4-25.x | |---------|------------|---------|--------------|--------------|---------------|------------------| | Camera Recording | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Screen Recording | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Real-time MP4 Transcoding | ✅ | ✅ | ✅ | ✅ | ❌ (WebM) | ⚠️ (Partial) | | Audio Level Analysis | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Source Switching | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Device Switching | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Persistent Upload Queue | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Unsupported Browsers
- Internet Explorer (all versions)
- Firefox < 76 (missing AudioWorklet)
- 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
- Samsung Internet 18.0+ - Full support
- Firefox Android 130+ - Full support
- Safari iOS 16.4+ - Partial support (WebCodecs partial)
- Safari iOS 26.0+ - Full support
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
