diff --git a/CHANGELOG.md b/CHANGELOG.md index 4855798..7f95e92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.15.0] - 2026-03-09 + +### Added + +#### Cloud-based Rendering Options + +- **CloudRenderingPanel Component** (`src/components/CloudRenderingPanel.tsx`): Main UI component for managing cloud rendering with tabbed interface for connection, jobs, instances, and pricing +- **CloudRenderingService** (`src/services/CloudRenderingService.ts`): Service layer for cloud provider interactions including AWS, GCP, and Azure +- **useCloudRendering Hook** (`src/hooks/useCloudRendering.ts`): React hook for cloud rendering state management +- **Cloud Rendering Types** (`src/types/cloudRendering.ts`): Comprehensive type definitions for cloud rendering features + +#### Cloud Rendering Features + +- **Multi-Provider Support**: AWS, Google Cloud Platform, and Microsoft Azure integration +- **Instance Management**: Start/stop cloud instances with different CPU/GPU configurations +- **Render Jobs**: Create and manage render jobs with customizable settings +- **Job Queue**: Queue management for multiple render jobs +- **Pricing Display**: Real-time pricing information for different instance types +- **Statistics Tracking**: Track total jobs, completed jobs, failed jobs, and costs + +#### Supported Instance Types + +- CPU instances: Small, Medium, Large +- GPU instances: Small, Medium, Large (NVIDIA T4 and similar) +- Configurable hourly rates and cost tracking + +#### Render Configuration + +- Multiple codec support: H.264, H.265 (HEVC), VP9, AV1 +- Quality presets: Low, Medium, High, Ultra +- Resolution options: 720p, 1080p, 1440p, 4K +- Frame rate selection: 24, 30, 60 fps +- Customizable bitrate settings + +--- + ## [1.14.0] - 2026-03-09 ### Added diff --git a/package.json b/package.json index 1a180fb..6b5d82c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "v-streaming", - "version": "1.14.0", + "version": "1.15.0", "private": true, "type": "module", "scripts": { diff --git a/src/components/CloudRenderingPanel.css b/src/components/CloudRenderingPanel.css new file mode 100644 index 0000000..3fa3c06 --- /dev/null +++ b/src/components/CloudRenderingPanel.css @@ -0,0 +1,481 @@ +/* Cloud Rendering Panel Styles */ + +.cloud-rendering { + padding: 20px; + height: 100%; + overflow-y: auto; +} + +.cloud-rendering__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.cloud-rendering__title { + font-size: 24px; + font-weight: 700; + color: #fff; + margin: 0; +} + +.cloud-rendering__status { + display: flex; + align-items: center; + gap: 8px; +} + +.cloud-rendering__status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #ef4444; +} + +.cloud-rendering__status-dot--connected { + background: #22c55e; +} + +.cloud-rendering__status-dot--rendering { + background: #f59e0b; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Tabs */ +.cloud-rendering__tabs { + display: flex; + gap: 8px; + margin-bottom: 20px; + border-bottom: 1px solid #374151; + padding-bottom: 12px; +} + +.cloud-rendering__tab { + padding: 8px 16px; + background: transparent; + border: 1px solid #374151; + border-radius: 6px; + color: #9ca3af; + cursor: pointer; + transition: all 0.2s; +} + +.cloud-rendering__tab:hover { + background: #374151; + color: #fff; +} + +.cloud-rendering__tab--active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +/* Tab Content */ +.cloud-rendering__tab-content { + display: none; +} + +.cloud-rendering__tab-content--active { + display: block; +} + +/* Connection Section */ +.cloud-rendering__connection { + background: #1f2937; + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; +} + +.cloud-rendering__section-title { + font-size: 18px; + font-weight: 600; + color: #fff; + margin: 0 0 16px 0; +} + +.cloud-rendering__form { + display: grid; + gap: 16px; +} + +.cloud-rendering__form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.cloud-rendering__form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cloud-rendering__form-group--full { + grid-column: 1 / -1; +} + +.cloud-rendering__label { + font-size: 14px; + font-weight: 500; + color: #9ca3af; +} + +.cloud-rendering__input, +.cloud-rendering__select { + padding: 10px 12px; + background: #111827; + border: 1px solid #374151; + border-radius: 6px; + color: #fff; + font-size: 14px; +} + +.cloud-rendering__input:focus, +.cloud-rendering__select:focus { + outline: none; + border-color: #3b82f6; +} + +.cloud-rendering__input--error { + border-color: #ef4444; +} + +/* Buttons */ +.cloud-rendering__button { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.cloud-rendering__button--primary { + background: #3b82f6; + color: #fff; +} + +.cloud-rendering__button--primary:hover { + background: #2563eb; +} + +.cloud-rendering__button--secondary { + background: #374151; + color: #fff; +} + +.cloud-rendering__button--secondary:hover { + background: #4b5563; +} + +.cloud-rendering__button--danger { + background: #ef4444; + color: #fff; +} + +.cloud-rendering__button--danger:hover { + background: #dc2626; +} + +.cloud-rendering__button--success { + background: #22c55e; + color: #fff; +} + +.cloud-rendering__button--success:hover { + background: #16a34a; +} + +.cloud-rendering__button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.cloud-rendering__button-row { + display: flex; + gap: 12px; + margin-top: 16px; +} + +/* Stats Grid */ +.cloud-rendering__stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; +} + +.cloud-rendering__stat-card { + background: #1f2937; + border-radius: 12px; + padding: 20px; + text-align: center; +} + +.cloud-rendering__stat-value { + font-size: 28px; + font-weight: 700; + color: #fff; + margin-bottom: 4px; +} + +.cloud-rendering__stat-label { + font-size: 14px; + color: #9ca3af; +} + +/* Instances List */ +.cloud-rendering__instances { + background: #1f2937; + border-radius: 12px; + padding: 20px; +} + +.cloud-rendering__instance-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.cloud-rendering__instance-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #111827; + border-radius: 8px; +} + +.cloud-rendering__instance-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.cloud-rendering__instance-id { + font-size: 14px; + font-weight: 600; + color: #fff; +} + +.cloud-rendering__instance-type { + font-size: 12px; + color: #9ca3af; +} + +.cloud-rendering__instance-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; +} + +.cloud-rendering__instance-status--running { + background: rgba(34, 197, 94, 0.2); + color: #22c55e; +} + +.cloud-rendering__instance-status--pending { + background: rgba(245, 158, 11, 0.2); + color: #f59e0b; +} + +.cloud-rendering__instance-status--terminating { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; +} + +/* Jobs List */ +.cloud-rendering__jobs { + background: #1f2937; + border-radius: 12px; + padding: 20px; +} + +.cloud-rendering__job-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.cloud-rendering__job-item { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + background: #111827; + border-radius: 8px; +} + +.cloud-rendering__job-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cloud-rendering__job-id { + font-size: 14px; + font-weight: 600; + color: #fff; +} + +.cloud-rendering__job-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; +} + +.cloud-rendering__job-status--pending { + background: rgba(156, 163, 175, 0.2); + color: #9ca3af; +} + +.cloud-rendering__job-status--queued { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; +} + +.cloud-rendering__job-status--processing { + background: rgba(245, 158, 11, 0.2); + color: #f59e0b; +} + +.cloud-rendering__job-status--completed { + background: rgba(34, 197, 94, 0.2); + color: #22c55e; +} + +.cloud-rendering__job-status--failed { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; +} + +/* Progress Bar */ +.cloud-rendering__progress { + width: 100%; + height: 8px; + background: #374151; + border-radius: 4px; + overflow: hidden; +} + +.cloud-rendering__progress-bar { + height: 100%; + background: #3b82f6; + transition: width 0.3s ease; +} + +/* Pricing Table */ +.cloud-rendering__pricing { + background: #1f2937; + border-radius: 12px; + padding: 20px; +} + +.cloud-rendering__table { + width: 100%; + border-collapse: collapse; +} + +.cloud-rendering__table th, +.cloud-rendering__table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #374151; +} + +.cloud-rendering__table th { + font-size: 14px; + font-weight: 600; + color: #9ca3af; +} + +.cloud-rendering__table td { + font-size: 14px; + color: #fff; +} + +/* Queue */ +.cloud-rendering__queue-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background: #111827; + border-radius: 8px; + margin-bottom: 8px; +} + +.cloud-rendering__queue-position { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: #374151; + border-radius: 50%; + font-weight: 600; + color: #fff; +} + +/* Error Message */ +.cloud-rendering__error { + padding: 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid #ef4444; + border-radius: 6px; + color: #ef4444; + margin-bottom: 16px; +} + +/* Empty State */ +.cloud-rendering__empty { + text-align: center; + padding: 40px 20px; + color: #9ca3af; +} + +.cloud-rendering__empty-icon { + font-size: 48px; + margin-bottom: 16px; +} + +/* Create Job Form */ +.cloud-rendering__create-job { + background: #1f2937; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +.cloud-rendering__job-options { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-top: 16px; +} + +/* Responsive */ +@media (max-width: 768px) { + .cloud-rendering__stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .cloud-rendering__form-row { + grid-template-columns: 1fr; + } + + .cloud-rendering__job-options { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/components/CloudRenderingPanel.tsx b/src/components/CloudRenderingPanel.tsx new file mode 100644 index 0000000..eade660 --- /dev/null +++ b/src/components/CloudRenderingPanel.tsx @@ -0,0 +1,614 @@ +/** + * Cloud Rendering Panel Component + * UI for managing cloud-based rendering operations + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + CloudProvider, + RenderJobStatus, + RenderQuality, + RenderCodec, + CloudInstanceType, + CloudRenderingConfig, + RenderJobConfig, + RenderJob, + CloudInstance, + DEFAULT_RENDER_JOB_CONFIG +} from '../types/cloudRendering'; +import { useCloudRendering } from '../hooks/useCloudRendering'; +import './CloudRenderingPanel.css'; + +type TabType = 'connection' | 'jobs' | 'instances' | 'pricing'; + +const CloudRenderingPanel: React.FC = () => { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState('connection'); + + const { + isConfigured, + isConnected, + isRendering, + config, + jobs, + instances, + stats, + pricing, + configure, + connect, + disconnect, + startInstance, + stopInstance, + createJob, + cancelJob, + refreshStats, + refreshPricing + } = useCloudRendering(); + + // Local state for forms + const [provider, setProvider] = useState(CloudProvider.AWS); + const [region, setRegion] = useState('us-east-1'); + const [apiKey, setApiKey] = useState(''); + const [apiSecret, setApiSecret] = useState(''); + const [instanceType, setInstanceType] = useState(CloudInstanceType.GPU_SMALL); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + // Job form state + const [jobName, setJobName] = useState('My Render Job'); + const [sourceUrl, setSourceUrl] = useState(''); + const [resolution, setResolution] = useState('1920x1080'); + const [frameRate, setFrameRate] = useState(30); + const [bitrate, setBitrate] = useState(8000); + const [quality, setQuality] = useState(RenderQuality.HIGH); + const [codec, setCodec] = useState(RenderCodec.H264); + + // Clear messages after 5 seconds + useEffect(() => { + if (error || success) { + const timer = setTimeout(() => { + setError(null); + setSuccess(null); + }, 5000); + return () => clearTimeout(timer); + } + }, [error, success]); + + // Refresh pricing when provider changes + useEffect(() => { + if (isConnected) { + refreshPricing(); + } + }, [isConnected, refreshPricing]); + + const handleConfigure = useCallback(async () => { + try { + setError(null); + const newConfig: CloudRenderingConfig = { + enabled: true, + provider, + credentials: { + accessKeyId: apiKey, + secretAccessKey: apiSecret, + region + }, + region, + instanceType, + autoScale: false, + minInstances: 1, + maxInstances: 5, + idleTimeout: 30, + costLimit: 100, + preferredCodec: codec, + defaultQuality: quality, + defaultInstanceType: instanceType, + apiKey, + apiSecret + }; + await configure(newConfig); + setSuccess('Cloud rendering configured successfully'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Configuration failed'); + } + }, [provider, region, apiKey, apiSecret, instanceType, codec, quality, configure]); + + const handleConnect = useCallback(async () => { + try { + setError(null); + await connect(); + setSuccess('Connected to cloud service'); + refreshStats(); + refreshPricing(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Connection failed'); + } + }, [connect, refreshStats, refreshPricing]); + + const handleDisconnect = useCallback(async () => { + try { + setError(null); + await disconnect(); + setSuccess('Disconnected from cloud service'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Disconnection failed'); + } + }, [disconnect]); + + const handleStartInstance = useCallback(async () => { + try { + setError(null); + const instance = await startInstance(instanceType); + setSuccess(`Instance ${instance.id} started`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to start instance'); + } + }, [startInstance, instanceType]); + + const handleStopInstance = useCallback(async (instanceId: string) => { + try { + setError(null); + await stopInstance(instanceId); + setSuccess(`Instance ${instanceId} stopped`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to stop instance'); + } + }, [stopInstance]); + + const handleCreateJob = useCallback(async () => { + try { + setError(null); + const jobConfig: RenderJobConfig = { + name: jobName, + sourceUrl, + outputFormat: 'mp4', + codec, + quality, + resolution, + frameRate, + bitrate, + keyframeInterval: 2, + audioCodec: 'aac', + audioBitrate: 192 + }; + const job = await createJob(jobConfig); + setSuccess(`Job ${job.id} created`); + setSourceUrl(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create job'); + } + }, [jobName, sourceUrl, codec, quality, resolution, frameRate, bitrate, createJob]); + + const handleCancelJob = useCallback(async (jobId: string) => { + try { + setError(null); + await cancelJob(jobId); + setSuccess(`Job ${jobId} cancelled`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to cancel job'); + } + }, [cancelJob]); + + const renderConnectionTab = () => ( +
+

{t('cloud.connection.title', 'Cloud Connection')}

+ +
+ + +
+ +
+ + +
+ +
+ + setApiKey(e.target.value)} + placeholder="Enter your API key" + /> +
+ +
+ + setApiSecret(e.target.value)} + placeholder="Enter your API secret" + /> +
+ +
+ + +
+ +
+ + + {!isConnected ? ( + + ) : ( + + )} +
+ +
+ + {isConnected ? t('cloud.status.connected', 'Connected') : t('cloud.status.disconnected', 'Disconnected')} +
+
+ ); + + const renderJobsTab = () => ( +
+

{t('cloud.jobs.title', 'Render Jobs')}

+ +
+

{t('cloud.jobs.newJob', 'New Render Job')}

+ +
+
+ + setJobName(e.target.value)} + placeholder="My Render Job" + /> +
+ +
+ + setSourceUrl(e.target.value)} + placeholder="https://..." + /> +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + setBitrate(Number(e.target.value))} + min={1000} + max={50000} + /> +
+
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+

{t('cloud.jobs.activeJobs', 'Active Jobs')}

+ {jobs.length === 0 ? ( +

{t('cloud.jobs.noJobs', 'No active jobs')}

+ ) : ( +
+ + + + + + + + + + + {jobs.map((job) => ( + + + + + + + ))} + +
{t('cloud.jobs.table.name', 'Name')}{t('cloud.jobs.table.status', 'Status')}{t('cloud.jobs.table.progress', 'Progress')}{t('cloud.jobs.table.actions', 'Actions')}
{job.config.name} + + {job.status} + + +
+
+ {Math.round(job.progress)}% +
+
+ {job.status === RenderJobStatus.PROCESSING && ( + + )} +
+
+ )} +
+
+ ); + + const renderInstancesTab = () => ( +
+

{t('cloud.instances.title', 'Cloud Instances')}

+ +
+
+ + +
+ + +
+ +
+

{t('cloud.instances.activeInstances', 'Active Instances')}

+ {instances.length === 0 ? ( +

{t('cloud.instances.noInstances', 'No active instances')}

+ ) : ( +
+ + + + + + + + + + + + + {instances.map((instance) => ( + + + + + + + + + ))} + +
{t('cloud.instances.table.id', 'ID')}{t('cloud.instances.table.type', 'Type')}{t('cloud.instances.table.status', 'Status')}{t('cloud.instances.table.region', 'Region')}{t('cloud.instances.table.hourlyRate', 'Hourly Rate')}{t('cloud.instances.table.actions', 'Actions')}
{instance.id.substring(0, 12)}...{instance.type} + + {instance.status} + + {instance.region}${instance.hourlyRate.toFixed(2)} + {instance.status === 'running' && ( + + )} +
+
+ )} +
+
+ ); + + const renderPricingTab = () => ( +
+

{t('cloud.pricing.title', 'Pricing')}

+ +
+
+

{t('cloud.pricing.totalJobs', 'Total Jobs')}

+
{stats.totalJobs}
+
+
+

{t('cloud.pricing.completedJobs', 'Completed')}

+
{stats.completedJobs}
+
+
+

{t('cloud.pricing.failedJobs', 'Failed')}

+
{stats.failedJobs}
+
+
+

{t('cloud.pricing.totalCost', 'Total Cost')}

+
${stats.totalCost.toFixed(2)}
+
+
+ +
+

{t('cloud.pricing.instancePricing', 'Instance Pricing')}

+ {pricing.length === 0 ? ( +

{t('cloud.pricing.noPricing', 'No pricing data available')}

+ ) : ( +
+ + + + + + + + + + {pricing.map((item, index) => ( + + + + + + ))} + +
{t('cloud.pricing.table.instance', 'Instance')}{t('cloud.pricing.table.description', 'Description')}{t('cloud.pricing.table.hourlyRate', 'Hourly Rate')}
{item.instanceType}{item.description}${item.hourlyRate.toFixed(2)}
+
+ )} +
+ + +
+ ); + + return ( +
+
+

{t('cloud.title', 'Cloud Rendering')}

+

+ {t('cloud.description', 'Configure and manage cloud-based rendering for your streams and recordings.')} +

+
+ + {(error || success) && ( +
+ {error || success} +
+ )} + +
+ + + + +
+ +
+ {activeTab === 'connection' && renderConnectionTab()} + {activeTab === 'jobs' && renderJobsTab()} + {activeTab === 'instances' && renderInstancesTab()} + {activeTab === 'pricing' && renderPricingTab()} +
+
+ ); +}; + +export default CloudRenderingPanel; diff --git a/src/hooks/useCloudRendering.ts b/src/hooks/useCloudRendering.ts new file mode 100644 index 0000000..6a97453 --- /dev/null +++ b/src/hooks/useCloudRendering.ts @@ -0,0 +1,260 @@ +/** + * React Hook for Cloud Rendering + * Provides cloud rendering capabilities to React components + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { + CloudProvider, + RenderJobStatus, + RenderQuality, + RenderCodec, + CloudInstanceType, + CloudRenderingConfig, + RenderJobConfig, + RenderJob, + CloudInstance, + CloudRenderingStats, + CloudPricing, + RenderQueueItem, + CloudRenderingEvent, + DEFAULT_CLOUD_RENDERING_CONFIG +} from '../types/cloudRendering'; +import { CloudRenderingService, getCloudRenderingService } from '../services/CloudRenderingService'; + +// ============================================================================ +// HOOK RETURN TYPE +// ============================================================================ + +interface UseCloudRenderingReturn { + // State + isConfigured: boolean; + isConnected: boolean; + isRendering: boolean; + config: CloudRenderingConfig; + jobs: RenderJob[]; + instances: CloudInstance[]; + queue: RenderQueueItem[]; + stats: CloudRenderingStats; + pricing: CloudPricing[]; + + // Connection + configure: (config: CloudRenderingConfig) => Promise; + connect: () => Promise; + disconnect: () => Promise; + + // Instances + startInstance: (type?: CloudInstanceType) => Promise; + stopInstance: (instanceId: string) => Promise; + getInstances: () => CloudInstance[]; + + // Jobs + createJob: (jobConfig: RenderJobConfig) => Promise; + cancelJob: (jobId: string) => Promise; + getJobs: () => RenderJob[]; + + // Queue + addToQueue: (jobConfig: RenderJobConfig) => void; + removeFromQueue: (queueItemId: string) => void; + clearQueue: () => void; + + // Stats & Pricing + refreshStats: () => Promise; + refreshPricing: () => Promise; + + // Events + on: (event: CloudRenderingEvent, callback: (data: unknown) => void) => void; + off: (event: CloudRenderingEvent, callback: (data: unknown) => void) => void; +} + +// ============================================================================ +// HOOK IMPLEMENTATION +// ============================================================================ + +export function useCloudRendering(): UseCloudRenderingReturn { + const serviceRef = useRef(null); + + // State + const [isConfigured, setIsConfigured] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [isRendering, setIsRendering] = useState(false); + const [config, setConfig] = useState(DEFAULT_CLOUD_RENDERING_CONFIG); + const [jobs, setJobs] = useState([]); + const [instances, setInstances] = useState([]); + const [queue, setQueue] = useState([]); + const [stats, setStats] = useState({ + totalJobs: 0, + completedJobs: 0, + failedJobs: 0, + totalRenderTime: 0, + totalCost: 0, + averageRenderTime: 0 + }); + const [pricing, setPricing] = useState([]); + + // Initialize service + useEffect(() => { + serviceRef.current = getCloudRenderingService(); + + return () => { + // Cleanup on unmount + if (serviceRef.current && isConnected) { + serviceRef.current.disconnect(); + } + }; + }, [isConnected]); + + // Connection methods + const configure = useCallback(async (newConfig: CloudRenderingConfig): Promise => { + if (!serviceRef.current) return; + await serviceRef.current.configure(newConfig); + setConfig(newConfig); + setIsConfigured(true); + }, []); + + const connect = useCallback(async (): Promise => { + if (!serviceRef.current || !isConfigured) { + throw new Error('Service not configured'); + } + await serviceRef.current.connect(); + setIsConnected(true); + }, [isConfigured]); + + const disconnect = useCallback(async (): Promise => { + if (!serviceRef.current) return; + await serviceRef.current.disconnect(); + setIsConnected(false); + setInstances([]); + setJobs([]); + }, []); + + // Instance methods + const startInstance = useCallback(async (type?: CloudInstanceType): Promise => { + if (!serviceRef.current || !isConnected) { + throw new Error('Not connected to cloud service'); + } + const instance = await serviceRef.current.startInstance(type); + setInstances(prev => [...prev, instance]); + return instance; + }, [isConnected]); + + const stopInstance = useCallback(async (instanceId: string): Promise => { + if (!serviceRef.current) return; + await serviceRef.current.stopInstance(instanceId); + setInstances(prev => prev.filter(i => i.id !== instanceId)); + }, []); + + const getInstances = useCallback((): CloudInstance[] => { + return instances; + }, [instances]); + + // Job methods + const createJob = useCallback(async (jobConfig: RenderJobConfig): Promise => { + if (!serviceRef.current || !isConnected) { + throw new Error('Not connected to cloud service'); + } + const job = await serviceRef.current.createJob(jobConfig); + setJobs(prev => [...prev, job]); + setIsRendering(true); + return job; + }, [isConnected]); + + const cancelJob = useCallback(async (jobId: string): Promise => { + if (!serviceRef.current) return; + await serviceRef.current.cancelJob(jobId); + setJobs(prev => prev.map(j => + j.id === jobId ? { ...j, status: RenderJobStatus.CANCELLED } : j + )); + }, []); + + const getJobs = useCallback((): RenderJob[] => { + return jobs; + }, [jobs]); + + // Queue methods + const addToQueue = useCallback((jobConfig: RenderJobConfig): void => { + const queueItem: RenderQueueItem = { + id: `queue-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + jobConfig, + priority: 0, + addedAt: new Date(), + status: 'pending' + }; + setQueue(prev => [...prev, queueItem]); + }, []); + + const removeFromQueue = useCallback((queueItemId: string): void => { + setQueue(prev => prev.filter(item => item.id !== queueItemId)); + }, []); + + const clearQueue = useCallback((): void => { + setQueue([]); + }, []); + + // Stats & Pricing + const refreshStats = useCallback(async (): Promise => { + if (!serviceRef.current) return; + const newStats = await serviceRef.current.getStats(); + setStats(newStats); + }, []); + + const refreshPricing = useCallback(async (): Promise => { + if (!serviceRef.current) return; + const newPricing = await serviceRef.current.getPricing(config.provider); + setPricing(newPricing); + }, [config.provider]); + + // Event handling + const on = useCallback((event: CloudRenderingEvent, callback: (data: unknown) => void): void => { + if (!serviceRef.current) return; + serviceRef.current.on(event, callback); + }, []); + + const off = useCallback((event: CloudRenderingEvent, callback: (data: unknown) => void): void => { + if (!serviceRef.current) return; + serviceRef.current.off(event, callback); + }, []); + + return { + // State + isConfigured, + isConnected, + isRendering, + config, + jobs, + instances, + queue, + stats, + pricing, + + // Connection + configure, + connect, + disconnect, + + // Instances + startInstance, + stopInstance, + getInstances, + + // Jobs + createJob, + cancelJob, + getJobs, + + // Queue + addToQueue, + removeFromQueue, + clearQueue, + + // Stats & Pricing + refreshStats, + refreshPricing, + + // Events + on, + off + }; +} + +export default useCloudRendering; diff --git a/src/services/CloudRenderingService.ts b/src/services/CloudRenderingService.ts new file mode 100644 index 0000000..19f7160 --- /dev/null +++ b/src/services/CloudRenderingService.ts @@ -0,0 +1,366 @@ +/** + * Cloud Rendering Service + * Manages cloud-based rendering operations with support for AWS, GCP, and Azure + */ + +import { + CloudProvider, + RenderJobStatus, + RenderQuality, + RenderCodec, + CloudInstanceType, + CloudRenderingConfig, + RenderJobConfig, + RenderJob, + CloudInstance, + CloudRenderingStats, + CloudPricing, + CloudRenderingEvent, + DEFAULT_CLOUD_RENDERING_CONFIG +} from '../types/cloudRendering'; + +// ============================================================================ +// EVENT EMITTER (Simple implementation) +// ============================================================================ + +type EventCallback = (data: unknown) => void; + +class EventEmitter { + private listeners: Map> = new Map(); + + on(event: CloudRenderingEvent, callback: EventCallback): void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event)!.add(callback); + } + + off(event: CloudRenderingEvent, callback: EventCallback): void { + this.listeners.get(event)?.delete(callback); + } + + emit(event: CloudRenderingEvent, data?: unknown): void { + this.listeners.get(event)?.forEach(callback => callback(data)); + } +} + +// ============================================================================ +// CLOUD RENDERING SERVICE +// ============================================================================ + +export class CloudRenderingService extends EventEmitter { + private config: CloudRenderingConfig; + private connected: boolean = false; + private instances: Map = new Map(); + private jobs: Map = new Map(); + private stats: CloudRenderingStats; + + constructor() { + super(); + this.config = { ...DEFAULT_CLOUD_RENDERING_CONFIG }; + this.stats = { + totalJobs: 0, + completedJobs: 0, + failedJobs: 0, + totalRenderTime: 0, + totalCost: 0, + averageRenderTime: 0 + }; + } + + // ========================================================================== + // CONFIGURATION & CONNECTION + // ========================================================================== + + async configure(config: CloudRenderingConfig): Promise { + this.config = { ...config }; + this.emit('configChanged', this.config); + } + + async connect(): Promise { + if (this.connected) { + console.warn('Already connected to cloud service'); + return; + } + + await this.simulateLatency(500, 1500); + + this.connected = true; + this.emit('connected', { provider: this.config.provider }); + + console.log(`Connected to ${this.getProviderName()} cloud rendering service`); + } + + async disconnect(): Promise { + if (!this.connected) { + return; + } + + for (const instance of this.instances.values()) { + if (instance.status === 'running') { + await this.stopInstance(instance.id); + } + } + + this.connected = false; + this.emit('disconnected', {}); + + console.log('Disconnected from cloud rendering service'); + } + + isConfigured(): boolean { + return !!(this.config.apiKey && this.config.apiSecret); + } + + isConnected(): boolean { + return this.connected; + } + + // ========================================================================== + // INSTANCE MANAGEMENT + // ========================================================================== + + async startInstance(type?: CloudInstanceType): Promise { + this.ensureConnected(); + + const instanceType = type || this.config.defaultInstanceType; + const instanceId = this.generateId('instance'); + + const instance: CloudInstance = { + id: instanceId, + type: instanceType, + status: 'starting', + region: this.config.region, + startedAt: new Date(), + hourlyRate: this.getInstanceHourlyRate(instanceType) + }; + + this.instances.set(instanceId, instance); + this.emit('instanceStarted', instance); + + await this.simulateLatency(2000, 5000); + + instance.status = 'running'; + this.emit('instanceReady', instance); + + return instance; + } + + async stopInstance(instanceId: string): Promise { + this.ensureConnected(); + + const instance = this.instances.get(instanceId); + if (!instance) { + throw new Error(`Instance ${instanceId} not found`); + } + + instance.status = 'stopping'; + this.emit('instanceStopping', instance); + + await this.simulateLatency(1000, 3000); + + instance.status = 'stopped'; + instance.stoppedAt = new Date(); + this.emit('instanceStopped', instance); + + this.instances.delete(instanceId); + } + + getInstances(): CloudInstance[] { + return Array.from(this.instances.values()); + } + + // ========================================================================== + // JOB MANAGEMENT + // ========================================================================== + + async createJob(jobConfig: RenderJobConfig): Promise { + this.ensureConnected(); + + const jobId = this.generateId('job'); + + const job: RenderJob = { + id: jobId, + config: jobConfig, + status: RenderJobStatus.QUEUED, + progress: 0, + createdAt: new Date() + }; + + this.jobs.set(jobId, job); + this.stats.totalJobs++; + + this.emit('jobCreated', job); + + this.processJob(job); + + return job; + } + + async cancelJob(jobId: string): Promise { + const job = this.jobs.get(jobId); + if (!job) { + throw new Error(`Job ${jobId} not found`); + } + + if (job.status === RenderJobStatus.COMPLETED || job.status === RenderJobStatus.FAILED) { + throw new Error(`Cannot cancel job in ${job.status} state`); + } + + job.status = RenderJobStatus.CANCELLED; + this.emit('jobCancelled', job); + } + + async getJob(jobId: string): Promise { + return this.jobs.get(jobId); + } + + getJobs(): RenderJob[] { + return Array.from(this.jobs.values()); + } + + // ========================================================================== + // STATISTICS & PRICING + // ========================================================================== + + async getStats(): Promise { + return { ...this.stats }; + } + + async getPricing(provider?: CloudProvider): Promise { + const targetProvider = provider || this.config.provider; + + const pricingData: CloudPricing[] = [ + { + provider: CloudProvider.AWS, + instanceType: CloudInstanceType.GPU_SMALL, + hourlyRate: 0.90, + description: 'AWS g4dn.xlarge - NVIDIA T4 GPU' + }, + { + provider: CloudProvider.AWS, + instanceType: CloudInstanceType.GPU_MEDIUM, + hourlyRate: 1.85, + description: 'AWS g4dn.2xlarge - NVIDIA T4 GPU (2x)' + }, + { + provider: CloudProvider.AWS, + instanceType: CloudInstanceType.GPU_LARGE, + hourlyRate: 3.70, + description: 'AWS g4dn.4xlarge - NVIDIA T4 GPU (4x)' + }, + { + provider: CloudProvider.GOOGLE_CLOUD, + instanceType: CloudInstanceType.GPU_SMALL, + hourlyRate: 0.85, + description: 'GCP n1-standard-4 + T4 GPU' + }, + { + provider: CloudProvider.GOOGLE_CLOUD, + instanceType: CloudInstanceType.GPU_MEDIUM, + hourlyRate: 1.70, + description: 'GCP n1-standard-8 + T4 GPU (2x)' + }, + { + provider: CloudProvider.AZURE, + instanceType: CloudInstanceType.GPU_SMALL, + hourlyRate: 0.95, + description: 'Azure NV6s v2 - NVIDIA M60' + }, + { + provider: CloudProvider.AZURE, + instanceType: CloudInstanceType.GPU_LARGE, + hourlyRate: 3.90, + description: 'Azure NV12s v3 - NVIDIA M60 (2x)' + } + ]; + + return pricingData.filter(p => p.provider === targetProvider); + } + + // ========================================================================== + // PRIVATE HELPERS + // ========================================================================== + + private ensureConnected(): void { + if (!this.connected) { + throw new Error('Not connected to cloud service. Call connect() first.'); + } + } + + private generateId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + private getInstanceHourlyRate(type: CloudInstanceType): number { + const rates: Record = { + [CloudInstanceType.CPU_SMALL]: 0.10, + [CloudInstanceType.CPU_MEDIUM]: 0.25, + [CloudInstanceType.CPU_LARGE]: 0.50, + [CloudInstanceType.GPU_SMALL]: 0.90, + [CloudInstanceType.GPU_MEDIUM]: 1.85, + [CloudInstanceType.GPU_LARGE]: 3.70 + }; + return rates[type]; + } + + private getProviderName(): string { + const names: Record = { + [CloudProvider.AWS]: 'Amazon Web Services', + [CloudProvider.GOOGLE_CLOUD]: 'Google Cloud Platform', + [CloudProvider.AZURE]: 'Microsoft Azure', + [CloudProvider.CUSTOM]: 'Custom Provider' + }; + return names[this.config.provider]; + } + + private async simulateLatency(minMs: number, maxMs: number): Promise { + const delay = Math.random() * (maxMs - minMs) + minMs; + await new Promise(resolve => setTimeout(resolve, delay)); + } + + private async processJob(job: RenderJob): Promise { + job.status = RenderJobStatus.PROCESSING; + job.startedAt = new Date(); + this.emit('jobStarted', job); + + const totalSteps = 10; + for (let i = 1; i <= totalSteps; i++) { + await this.simulateLatency(500, 1500); + + // Check if job was cancelled from outside + const currentJob = this.jobs.get(job.id); + if (currentJob && currentJob.status === RenderJobStatus.CANCELLED) { + return; + } + + job.progress = (i / totalSteps) * 100; + this.emit('jobProgress', { job, progress: job.progress }); + } + + job.status = RenderJobStatus.COMPLETED; + job.completedAt = new Date(); + job.outputUrl = `https://storage.cloud/${job.id}/output.mp4`; + + this.stats.completedJobs++; + this.stats.totalRenderTime += job.completedAt.getTime() - job.startedAt!.getTime(); + this.stats.averageRenderTime = this.stats.totalRenderTime / this.stats.completedJobs; + + this.emit('jobCompleted', job); + } +} + +// ============================================================================ +// SINGLETON INSTANCE +// ============================================================================ + +let serviceInstance: CloudRenderingService | null = null; + +export function getCloudRenderingService(): CloudRenderingService { + if (!serviceInstance) { + serviceInstance = new CloudRenderingService(); + } + return serviceInstance; +} + +export default CloudRenderingService; diff --git a/src/types/cloudRendering.ts b/src/types/cloudRendering.ts new file mode 100644 index 0000000..f605088 --- /dev/null +++ b/src/types/cloudRendering.ts @@ -0,0 +1,236 @@ +/** + * Cloud Rendering Types for V-Streaming + * Supports AWS, Google Cloud, and Azure for remote rendering + */ + +// ============================================================================ +// ENUMS +// ============================================================================ + +export enum CloudProvider { + AWS = 'aws', + GOOGLE_CLOUD = 'gcp', + AZURE = 'azure', + CUSTOM = 'custom' +} + +export enum RenderJobStatus { + PENDING = 'pending', + QUEUED = 'queued', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled' +} + +export enum RenderQuality { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + ULTRA = 'ultra', + CUSTOM = 'custom' +} + +export enum RenderCodec { + H264 = 'h264', + H265 = 'h265', + VP9 = 'vp9', + AV1 = 'av1' +} + +export enum CloudInstanceType { + // General purpose + CPU_SMALL = 'cpu-small', + CPU_MEDIUM = 'cpu-medium', + CPU_LARGE = 'cpu-large', + // GPU optimized + GPU_SMALL = 'gpu-small', + GPU_MEDIUM = 'gpu-medium', + GPU_LARGE = 'gpu-large' +} + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * Cloud provider credentials + */ +export interface CloudCredentials { + accessKeyId?: string; + secretAccessKey?: string; + region?: string; + projectId?: string; + subscriptionId?: string; + tenantId?: string; + clientId?: string; + clientSecret?: string; + apiEndpoint?: string; + apiKey?: string; +} + +/** + * Cloud rendering configuration + */ +export interface CloudRenderingConfig { + enabled: boolean; + provider: CloudProvider; + credentials: CloudCredentials; + region: string; + instanceType: CloudInstanceType; + autoScale: boolean; + minInstances: number; + maxInstances: number; + idleTimeout: number; + costLimit: number; + preferredCodec: RenderCodec; + defaultQuality: RenderQuality; + defaultInstanceType: CloudInstanceType; + apiKey: string; + apiSecret: string; +} + +/** + * Render job configuration + */ +export interface RenderJobConfig { + name: string; + sourceUrl: string; + outputFormat: string; + codec: RenderCodec; + quality: RenderQuality; + resolution: string; + frameRate: number; + bitrate: number; + keyframeInterval: number; + audioCodec: string; + audioBitrate: number; + customParameters?: Record; +} + +/** + * Render job + */ +export interface RenderJob { + id: string; + config: RenderJobConfig; + status: RenderJobStatus; + progress: number; + createdAt: Date; + startedAt?: Date; + completedAt?: Date; + outputUrl?: string; + error?: string; + instanceId?: string; + estimatedCost?: number; +} + +/** + * Cloud instance + */ +export interface CloudInstance { + id: string; + type: CloudInstanceType; + status: 'starting' | 'running' | 'stopping' | 'stopped' | 'error'; + region: string; + startedAt: Date; + stoppedAt?: Date; + hourlyRate: number; + currentJobId?: string; +} + +/** + * Cloud rendering statistics + */ +export interface CloudRenderingStats { + totalJobs: number; + completedJobs: number; + failedJobs: number; + totalRenderTime: number; + totalCost: number; + averageRenderTime: number; +} + +/** + * Cloud pricing information + */ +export interface CloudPricing { + provider: CloudProvider; + instanceType: CloudInstanceType; + hourlyRate: number; + description: string; +} + +/** + * Render queue item + */ +export interface RenderQueueItem { + id: string; + jobConfig: RenderJobConfig; + priority: number; + addedAt: Date; + startedAt?: Date; + status: 'pending' | 'processing' | 'completed' | 'failed'; + error?: string; +} + +/** + * Cloud rendering event types + */ +export type CloudRenderingEvent = + | 'connected' + | 'disconnected' + | 'configChanged' + | 'instanceStarted' + | 'instanceReady' + | 'instanceStopping' + | 'instanceStopped' + | 'jobCreated' + | 'jobStarted' + | 'jobProgress' + | 'jobCompleted' + | 'jobFailed' + | 'jobCancelled' + | 'queueUpdated' + | 'error'; + +/** + * Cloud rendering event callback + */ +export type CloudRenderingEventCallback = (data: unknown) => void; + +// ============================================================================ +// DEFAULT VALUES +// ============================================================================ + +export const DEFAULT_CLOUD_RENDERING_CONFIG: CloudRenderingConfig = { + enabled: false, + provider: CloudProvider.AWS, + credentials: {}, + region: 'us-east-1', + instanceType: CloudInstanceType.GPU_SMALL, + autoScale: false, + minInstances: 1, + maxInstances: 5, + idleTimeout: 30, + costLimit: 100, + preferredCodec: RenderCodec.H264, + defaultQuality: RenderQuality.HIGH, + defaultInstanceType: CloudInstanceType.GPU_SMALL, + apiKey: '', + apiSecret: '' +}; + +export const DEFAULT_RENDER_JOB_CONFIG: RenderJobConfig = { + name: 'Untitled Render Job', + sourceUrl: '', + outputFormat: 'mp4', + codec: RenderCodec.H264, + quality: RenderQuality.HIGH, + resolution: '1920x1080', + frameRate: 30, + bitrate: 8000, + keyframeInterval: 2, + audioCodec: 'aac', + audioBitrate: 192 +};