seedance-video-sdk
Version:
Seedance AI Video Generation SDK - Create professional videos from text and images using ByteDance's advanced AI technology
771 lines (763 loc) • 28.1 kB
JavaScript
'use strict';
var axios = require('axios');
var FormData = require('form-data');
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
/**
* Seedance AI Video Generation Client
*
* Official SDK for integrating Seedance AI video generation capabilities
* into your applications. Supports text-to-video and image-to-video generation
* with advanced features like multi-shot narrative and style control.
*
* @example
* ```typescript
* import { SeedanceClient } from '@seedance/video-sdk';
*
* const client = new SeedanceClient({
* apiKey: 'your-api-key'
* });
*
* // Generate video from text
* const task = await client.generateVideo({
* prompt: 'A beautiful sunset over the ocean',
* duration: 5,
* resolution: '1080p'
* });
*
* // Check progress
* const status = await client.getTask(task.taskId);
* ```
*/
class SeedanceClient {
constructor(config) {
this.config = {
baseURL: 'https://api.seedance.studio/v1',
timeout: 30000,
debug: false,
...config,
};
this.client = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': '@seedance/video-sdk/1.0.0',
},
});
// Add request/response interceptors for debugging and error handling
this.setupInterceptors();
}
/**
* Generate a video from text prompt or image
*/
async generateVideo(request) {
try {
const formData = new FormData();
// Add text prompt
formData.append('prompt', request.prompt);
// Add optional parameters
if (request.duration)
formData.append('duration', request.duration.toString());
if (request.resolution)
formData.append('resolution', request.resolution);
if (request.style)
formData.append('style', request.style);
if (request.aspectRatio)
formData.append('aspectRatio', request.aspectRatio);
if (request.frameRate)
formData.append('frameRate', request.frameRate.toString());
// Add generation parameters
if (request.parameters) {
formData.append('parameters', JSON.stringify(request.parameters));
}
// Add image if provided
if (request.image) {
if (request.image instanceof File) {
formData.append('image', request.image);
}
else if (typeof request.image === 'string') {
// URL or base64
formData.append('imageUrl', request.image);
}
else {
// Buffer
formData.append('image', request.image, 'image.jpg');
}
}
const response = await this.client.post('/videos/generate', formData, {
headers: {
...formData.getHeaders(),
},
});
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Get task status and details
*/
async getTask(taskId) {
try {
const response = await this.client.get(`/videos/tasks/${taskId}`);
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* List user's video tasks with pagination
*/
async listTasks(options = {}) {
try {
const params = new URLSearchParams();
if (options.page)
params.append('page', options.page.toString());
if (options.pageSize)
params.append('pageSize', options.pageSize.toString());
if (options.status)
params.append('status', options.status);
if (options.sortBy)
params.append('sortBy', options.sortBy);
if (options.sortOrder)
params.append('sortOrder', options.sortOrder);
const response = await this.client.get(`/videos/tasks?${params}`);
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Cancel a pending or processing task
*/
async cancelTask(taskId) {
try {
await this.client.post(`/videos/tasks/${taskId}/cancel`);
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Delete a completed task
*/
async deleteTask(taskId) {
try {
await this.client.delete(`/videos/tasks/${taskId}`);
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Upload an image for later use in video generation
*/
async uploadImage(image, filename) {
try {
const formData = new FormData();
formData.append('image', image, filename || 'image.jpg');
const response = await this.client.post('/upload/image', formData, {
headers: {
...formData.getHeaders(),
},
});
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Get user quota information
*/
async getQuota() {
try {
const response = await this.client.get('/user/quota');
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Get user information
*/
async getUserInfo() {
try {
const response = await this.client.get('/user/info');
return response.data;
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Configure webhook for task notifications
*/
async configureWebhook(config) {
try {
await this.client.post('/webhooks/configure', config);
}
catch (error) {
throw this.handleError(error);
}
}
/**
* Wait for task completion with polling
*/
async waitForCompletion(taskId, options = {}) {
const { pollInterval = 2000, timeout = 300000, onProgress } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const task = await this.getTask(taskId);
if (onProgress) {
onProgress(task);
}
if (task.status === 'completed') {
return task;
}
if (task.status === 'failed') {
throw new Error(`Task failed: ${task.error}`);
}
if (task.status === 'cancelled') {
throw new Error('Task was cancelled');
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error('Task timeout');
}
setupInterceptors() {
// Request interceptor for debugging
this.client.interceptors.request.use((config) => {
if (this.config.debug) {
console.log('Seedance SDK Request:', config);
}
return config;
}, (error) => {
if (this.config.debug) {
console.error('Seedance SDK Request Error:', error);
}
return Promise.reject(error);
});
// Response interceptor for debugging
this.client.interceptors.response.use((response) => {
if (this.config.debug) {
console.log('Seedance SDK Response:', response);
}
return response;
}, (error) => {
if (this.config.debug) {
console.error('Seedance SDK Response Error:', error);
}
return Promise.reject(error);
});
}
handleError(error) {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
return {
code: data?.code || `HTTP_${status}`,
message: data?.message || error.message,
details: data,
};
}
else if (error.request) {
// Network error
return {
code: 'NETWORK_ERROR',
message: 'Network request failed',
details: error.request,
};
}
else {
// Other error
return {
code: 'UNKNOWN_ERROR',
message: error.message,
details: error,
};
}
}
}
/**
* Utility functions for Seedance Video SDK
*/
/**
* Convert video resolution to dimensions
*/
function getResolutionDimensions(resolution) {
switch (resolution) {
case '480p':
return { width: 854, height: 480 };
case '720p':
return { width: 1280, height: 720 };
case '1080p':
return { width: 1920, height: 1080 };
case '4K':
return { width: 3840, height: 2160 };
default:
return { width: 1920, height: 1080 };
}
}
/**
* Calculate video dimensions based on resolution and aspect ratio
*/
function calculateVideoDimensions(resolution, aspectRatio) {
const baseDimensions = getResolutionDimensions(resolution);
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
// Calculate dimensions maintaining aspect ratio
const aspectValue = widthRatio / heightRatio;
if (aspectValue > 1) {
// Landscape
const width = baseDimensions.width;
const height = Math.round(width / aspectValue);
return { width, height };
}
else {
// Portrait or square
const height = baseDimensions.height;
const width = Math.round(height * aspectValue);
return { width, height };
}
}
/**
* Validate video generation parameters
*/
function validateVideoParams(params) {
const errors = [];
if (params.duration !== undefined) {
if (params.duration < 1 || params.duration > 30) {
errors.push('Duration must be between 1 and 30 seconds');
}
}
if (params.frameRate !== undefined) {
if (params.frameRate < 12 || params.frameRate > 60) {
errors.push('Frame rate must be between 12 and 60 fps');
}
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Format file size in human readable format
*/
function formatFileSize(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Format duration in human readable format
*/
function formatDuration(seconds) {
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (remainingSeconds === 0) {
return `${minutes}m`;
}
return `${minutes}m ${remainingSeconds}s`;
}
/**
* Get status color for UI components
*/
function getStatusColor(status) {
switch (status) {
case 'pending':
return '#f59e0b'; // amber
case 'processing':
return '#3b82f6'; // blue
case 'completed':
return '#10b981'; // green
case 'failed':
return '#ef4444'; // red
case 'cancelled':
return '#6b7280'; // gray
default:
return '#6b7280';
}
}
/**
* Get status display text
*/
function getStatusText(status) {
switch (status) {
case 'pending':
return 'Pending';
case 'processing':
return 'Processing';
case 'completed':
return 'Completed';
case 'failed':
return 'Failed';
case 'cancelled':
return 'Cancelled';
default:
return 'Unknown';
}
}
/**
* Estimate video file size based on parameters
*/
function estimateVideoSize(duration, resolution, frameRate = 30) {
const dimensions = getResolutionDimensions(resolution);
const pixels = dimensions.width * dimensions.height;
// Rough estimation: 0.1 bits per pixel per frame
const bitsPerFrame = pixels * 0.1;
const totalFrames = duration * frameRate;
const totalBits = bitsPerFrame * totalFrames;
return Math.round(totalBits / 8); // Convert to bytes
}
/**
* Generate a unique task ID (for client-side tracking)
*/
function generateTaskId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Check if a URL is valid
*/
function isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
/**
* Check if a file is a valid image
*/
function isValidImageFile(file) {
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
return validTypes.includes(file.type);
}
/**
* Get file extension from filename
*/
function getFileExtension(filename) {
return filename.split('.').pop()?.toLowerCase() || '';
}
/**
* Convert File to base64 string
*/
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
if (typeof reader.result === 'string') {
resolve(reader.result);
}
else {
reject(new Error('Failed to convert file to base64'));
}
};
reader.onerror = error => reject(error);
});
}
/**
* Debounce function for API calls
*/
function debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Retry function with exponential backoff
*/
async function retry(fn, options = {}) {
const { retries = 3, delay = 1000, backoff = 2 } = options;
try {
return await fn();
}
catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
return retry(fn, {
retries: retries - 1,
delay: delay * backoff,
backoff,
});
}
throw error;
}
}
/**
* Create a progress tracker for long-running operations
*/
class ProgressTracker {
constructor() {
this.callbacks = [];
this._progress = 0;
}
get progress() {
return this._progress;
}
setProgress(progress) {
this._progress = Math.max(0, Math.min(100, progress));
this.callbacks.forEach(callback => callback(this._progress));
}
onProgress(callback) {
this.callbacks.push(callback);
// Return unsubscribe function
return () => {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
};
}
reset() {
this._progress = 0;
this.callbacks.forEach(callback => callback(0));
}
}
/**
* Seedance Video Player Component
*
* A customizable video player optimized for AI-generated videos
* with built-in loading states and error handling.
*/
const VideoPlayer = ({ src, poster, controls = true, autoplay = false, loop = false, muted = false, className = '', onLoadStart, onLoadedData, onError, }) => {
const videoRef = react.useRef(null);
const [isLoading, setIsLoading] = react.useState(true);
const [hasError, setHasError] = react.useState(false);
const [errorMessage, setErrorMessage] = react.useState('');
react.useEffect(() => {
const video = videoRef.current;
if (!video)
return;
const handleLoadStart = () => {
setIsLoading(true);
setHasError(false);
onLoadStart?.();
};
const handleLoadedData = () => {
setIsLoading(false);
onLoadedData?.();
};
const handleError = (event) => {
setIsLoading(false);
setHasError(true);
const error = new Error('Video failed to load');
setErrorMessage(error.message);
onError?.(error);
};
video.addEventListener('loadstart', handleLoadStart);
video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('error', handleError);
return () => {
video.removeEventListener('loadstart', handleLoadStart);
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('error', handleError);
};
}, [onLoadStart, onLoadedData, onError]);
const baseStyles = `
relative w-full h-auto rounded-lg overflow-hidden bg-gray-900
`;
const videoStyles = `
w-full h-full object-cover
`;
const loadingStyles = `
absolute inset-0 flex items-center justify-center bg-gray-900 text-white
`;
const errorStyles = `
absolute inset-0 flex flex-col items-center justify-center bg-gray-900 text-white p-4 text-center
`;
return (jsxRuntime.jsxs("div", { className: `${baseStyles} ${className}`, children: [jsxRuntime.jsx("video", { ref: videoRef, src: src, poster: poster, controls: controls, autoPlay: autoplay, loop: loop, muted: muted, className: videoStyles, playsInline: true }), isLoading && (jsxRuntime.jsxs("div", { className: loadingStyles, children: [jsxRuntime.jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-white" }), jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: "Loading video..." })] })), hasError && (jsxRuntime.jsxs("div", { className: errorStyles, children: [jsxRuntime.jsx("svg", { className: "w-12 h-12 text-red-400 mb-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }), jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "Failed to load video" }), jsxRuntime.jsx("p", { className: "text-xs text-gray-400 mt-1", children: errorMessage })] }))] }));
};
/**
* Progress Indicator Component
*
* Displays the progress and status of video generation tasks
* with visual indicators and estimated time remaining.
*/
const ProgressIndicator = ({ progress, status, estimatedTime, className = '', showPercentage = true, showStatus = true, }) => {
const statusColor = getStatusColor(status);
const statusText = getStatusText(status);
const getProgressBarColor = (status) => {
switch (status) {
case 'processing':
return 'bg-blue-500';
case 'completed':
return 'bg-green-500';
case 'failed':
return 'bg-red-500';
case 'cancelled':
return 'bg-gray-500';
default:
return 'bg-yellow-500';
}
};
const getIcon = (status) => {
switch (status) {
case 'pending':
return (jsxRuntime.jsx("svg", { className: "w-4 h-4 animate-pulse", fill: "currentColor", viewBox: "0 0 20 20", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }));
case 'processing':
return (jsxRuntime.jsx("svg", { className: "w-4 h-4 animate-spin", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }));
case 'completed':
return (jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }));
case 'failed':
return (jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z", clipRule: "evenodd" }) }));
case 'cancelled':
return (jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z", clipRule: "evenodd" }) }));
default:
return null;
}
};
const baseStyles = `
w-full p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700
`;
return (jsxRuntime.jsxs("div", { className: `${baseStyles} ${className}`, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx("div", { style: { color: statusColor }, children: getIcon(status) }), showStatus && (jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-white", children: statusText }))] }), showPercentage && (jsxRuntime.jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [Math.round(progress), "%"] }))] }), jsxRuntime.jsx("div", { className: "w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-3", children: jsxRuntime.jsx("div", { className: `h-2 rounded-full transition-all duration-300 ease-out ${getProgressBarColor(status)}`, style: { width: `${Math.min(100, Math.max(0, progress))}%` } }) }), jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500 dark:text-gray-400", children: [jsxRuntime.jsxs("div", { children: [status === 'processing' && estimatedTime && (jsxRuntime.jsxs("span", { children: ["Estimated time: ", formatDuration(estimatedTime)] })), status === 'completed' && (jsxRuntime.jsx("span", { children: "Video generation completed" })), status === 'failed' && (jsxRuntime.jsx("span", { children: "Generation failed" })), status === 'pending' && (jsxRuntime.jsx("span", { children: "Waiting in queue..." })), status === 'cancelled' && (jsxRuntime.jsx("span", { children: "Generation cancelled" }))] }), status === 'processing' && (jsxRuntime.jsxs("div", { className: "flex items-center space-x-1", children: [jsxRuntime.jsx("div", { className: "w-1 h-1 bg-blue-500 rounded-full animate-pulse" }), jsxRuntime.jsx("div", { className: "w-1 h-1 bg-blue-500 rounded-full animate-pulse", style: { animationDelay: '0.2s' } }), jsxRuntime.jsx("div", { className: "w-1 h-1 bg-blue-500 rounded-full animate-pulse", style: { animationDelay: '0.4s' } })] }))] })] }));
};
/**
* @seedance/video-sdk
*
* Official Seedance AI Video Generation SDK
*
* Create professional videos from text and images using ByteDance's advanced AI technology.
* This SDK provides a simple and powerful interface for integrating Seedance's video generation
* capabilities into your applications.
*
* @example
* ```typescript
* import { SeedanceClient } from '@seedance/video-sdk';
*
* const client = new SeedanceClient({
* apiKey: 'your-api-key'
* });
*
* // Generate video from text
* const task = await client.generateVideo({
* prompt: 'A beautiful sunset over the ocean',
* duration: 5,
* resolution: '1080p'
* });
*
* // Wait for completion
* const completedTask = await client.waitForCompletion(task.taskId, {
* onProgress: (task) => console.log(`Progress: ${task.progress}%`)
* });
*
* console.log('Video URL:', completedTask.videoUrl);
* ```
*
* @example React Components
* ```tsx
* import { VideoPlayer, ProgressIndicator } from '@seedance/video-sdk/react';
*
* function MyComponent() {
* return (
* <div>
* <VideoPlayer
* src="https://example.com/video.mp4"
* controls
* autoplay
* />
* <ProgressIndicator
* progress={75}
* status="processing"
* estimatedTime={30}
* />
* </div>
* );
* }
* ```
*
* @author Seedance Studio
* @version 1.0.0
* @license MIT
* @homepage https://seedance.studio
* @repository https://github.com/saasfly/saasfly
*/
// Core client
// Constants
const SDK_VERSION = '1.0.0';
const DEFAULT_API_BASE_URL = 'https://api.seedance.studio/v1';
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_VIDEO_DURATION = 30; // seconds
const MIN_VIDEO_DURATION = 1; // seconds
/**
* Create a new Seedance client instance
*
* @param config - Configuration options
* @returns Configured SeedanceClient instance
*/
function createClient(config) {
return new SeedanceClient(config);
}
/**
* Quick video generation helper function
*
* @param apiKey - Your Seedance API key
* @param prompt - Text prompt for video generation
* @param options - Additional generation options
* @returns Promise resolving to video generation response
*/
async function generateVideo(apiKey, prompt, options = {}) {
const client = new SeedanceClient({ apiKey });
return client.generateVideo({ prompt, ...options });
}
/**
* Quick image-to-video generation helper function
*
* @param apiKey - Your Seedance API key
* @param image - Input image (File, URL, or Buffer)
* @param prompt - Text prompt describing the desired video
* @param options - Additional generation options
* @returns Promise resolving to video generation response
*/
async function generateVideoFromImage(apiKey, image, prompt, options = {}) {
const client = new SeedanceClient({ apiKey });
return client.generateVideo({ prompt, image, ...options });
}
// Note: No default export to avoid CommonJS/ESM compatibility issues
// Use: import { SeedanceClient } from '@seedance/video-sdk'
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
exports.MAX_FILE_SIZE = MAX_FILE_SIZE;
exports.MAX_VIDEO_DURATION = MAX_VIDEO_DURATION;
exports.MIN_VIDEO_DURATION = MIN_VIDEO_DURATION;
exports.ProgressIndicator = ProgressIndicator;
exports.ProgressTracker = ProgressTracker;
exports.SDK_VERSION = SDK_VERSION;
exports.SUPPORTED_IMAGE_FORMATS = SUPPORTED_IMAGE_FORMATS;
exports.SeedanceClient = SeedanceClient;
exports.VideoPlayer = VideoPlayer;
exports.calculateVideoDimensions = calculateVideoDimensions;
exports.createClient = createClient;
exports.debounce = debounce;
exports.estimateVideoSize = estimateVideoSize;
exports.fileToBase64 = fileToBase64;
exports.formatDuration = formatDuration;
exports.formatFileSize = formatFileSize;
exports.generateTaskId = generateTaskId;
exports.generateVideo = generateVideo;
exports.generateVideoFromImage = generateVideoFromImage;
exports.getFileExtension = getFileExtension;
exports.getResolutionDimensions = getResolutionDimensions;
exports.getStatusColor = getStatusColor;
exports.getStatusText = getStatusText;
exports.isValidImageFile = isValidImageFile;
exports.isValidUrl = isValidUrl;
exports.retry = retry;
exports.validateVideoParams = validateVideoParams;
//# sourceMappingURL=index.js.map