UNPKG

ems-editor

Version:

EMS Video Editor SDK - Universal JavaScript SDK for external video editor integration

973 lines (965 loc) โ€ข 36.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } let getRandomValues; const rnds8 = new Uint8Array(16); function rng() { if (!getRandomValues) { if (typeof crypto === 'undefined' || !crypto.getRandomValues) { throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); } getRandomValues = crypto.getRandomValues.bind(crypto); } return getRandomValues(rnds8); } const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native = { randomUUID }; function v4(options, buf, offset) { if (native.randomUUID && true && !options) { return native.randomUUID(); } options = options || {}; const rnds = options.random ?? options.rng?.() ?? rng(); if (rnds.length < 16) { throw new Error('Random bytes length must be >= 16'); } rnds[6] = (rnds[6] & 0x0f) | 0x40; rnds[8] = (rnds[8] & 0x3f) | 0x80; return unsafeStringify(rnds); } /** * EMS Editor SDK - Simple and Reliable * Handles communication between external apps and iframe editor */ class EMSEditor { constructor(config) { this.ws = null; this.connected = false; this.ready = false; this.eventListeners = new Map(); this.iframe = null; this.config = config; this.sessionId = config.sessionId || v4(); console.log('๐ŸŽฌ EMS Editor SDK initialized', { sessionId: this.sessionId }); this.connect(); } // Connection Management async connect() { const wsUrl = this.config.serverUrl.replace(/^http/, 'ws') + '/ws'; try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('โœ… Connected to WebSocket'); this.connected = true; this.registerSession(); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; this.ws.onclose = () => { console.log('๐Ÿ”Œ WebSocket disconnected'); this.connected = false; this.ready = false; this.emit('disconnected'); }; this.ws.onerror = (error) => { console.error('โŒ WebSocket error:', error); this.emit('error', error); }; } catch (error) { console.error('โŒ Connection failed:', error); this.emit('error', error); } } registerSession() { this.sendMessage({ type: 'REGISTER_SESSION', payload: { sessionId: this.sessionId, type: 'external-app' } }); } handleMessage(message) { console.log('๐Ÿ“จ Received:', message.type); switch (message.type) { case 'SESSION_REGISTERED': this.emit('connected'); break; case 'PARTNER_READY': this.ready = true; console.log('๐ŸŽ‰ Editor is ready!'); this.emit('ready'); break; case 'EDITOR_READY': this.emit('editorReady'); break; case 'MAM_STATUS': this.emit('mamStatus', message.payload); break; case 'THEME_CHANGED': this.emit('themeChanged', message.payload); break; case 'THEME_UPDATE': this.emit('themeUpdate', message.payload); break; case 'CUSTOMIZE_THEME': this.emit('customTheme', message.payload); break; case 'TEMPLATE_IMPORTED': this.emit('templateImported', message.payload); break; case 'RENDER_PROGRESS': this.emit('renderProgress', message.payload); break; case 'PIPELINE_EVENT': this.emit('pipelineEvent', message.payload); // Emit specific pipeline stage events if (message.payload.stage === 'uploading') { this.emit('mamUploadStarted', message.payload); } break; case 'MAM_UPLOAD_EVENT': console.log(`๐Ÿ“ค MAM Upload ${message.payload.stage}:`, message.payload); this.emit('mamUploadEvent', message.payload); // Emit specific MAM upload stage events for easier handling this.emit(`mamUpload${message.payload.stage.charAt(0).toUpperCase() + message.payload.stage.slice(1)}`, message.payload); break; case 'RENDER_COMPLETE': this.emit('renderComplete', message.payload); // Emit MAM upload completion if asset ID is present if (message.payload.mamAssetId) { this.emit('mamUploadComplete', { assetId: message.payload.mamAssetId, assetName: message.payload.mamAssetName, assetUrl: message.payload.mamAssetUrl, jobId: message.payload.jobId, videoUrl: message.payload.videoUrl }); } break; case 'WORKSPACES_CLEARED': this.emit('workspacesCleared', message.payload); break; case 'WORKSPACES_LIST': this.emit('workspacesListReceived', message.payload); break; case 'PARTNER_DISCONNECTED': this.ready = false; this.emit('editorDisconnected'); break; case 'ERROR': console.error('โŒ Server error:', message.payload.error); this.emit('error', message.payload.error); break; } } // Public API Methods /** * Set iframe reference for the editor */ setIframe(iframe) { this.iframe = iframe; iframe.src = `${this.config.serverUrl}?sessionId=${this.sessionId}`; console.log('๐Ÿ–ผ๏ธ Iframe set with sessionId:', this.sessionId); } /** * Login to MAM system */ async loginToMAM(mamConfig) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'MAM_LOGIN', sessionId: this.sessionId, payload: mamConfig }); } /** * Logout from MAM system */ async logoutFromMAM() { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'MAM_LOGOUT', sessionId: this.sessionId, payload: {} }); } /** * Set theme (light or dark) */ async setTheme(theme) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'SET_THEME', sessionId: this.sessionId, payload: { theme } }); } /** * Update theme with preset theme ID or custom theme object */ async updateTheme(themeId, customTheme) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'THEME_UPDATE', sessionId: this.sessionId, payload: { themeId, customTheme } }); } /** * Customize theme with primary, secondary, background colors */ async customizeTheme(themeConfig) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } // Generate a complete theme object from the basic colors const customTheme = { id: 'custom', name: 'Custom Theme', primary: this.generateColorPalette(themeConfig.primary || '#3b82f6'), secondary: this.generateColorPalette(themeConfig.secondary || '#64748b'), background: { primary: themeConfig.background || '#0f172a', secondary: this.adjustColor(themeConfig.background || '#0f172a', 10), tertiary: this.adjustColor(themeConfig.background || '#0f172a', 20), accent: this.adjustColor(themeConfig.background || '#0f172a', 30) }, text: { primary: '#f8fafc', secondary: '#e2e8f0', muted: '#94a3b8', accent: themeConfig.accent || '#60a5fa' }, border: { primary: this.adjustColor(themeConfig.background || '#0f172a', 40), secondary: this.adjustColor(themeConfig.background || '#0f172a', 50), accent: themeConfig.primary || '#3b82f6' }, shadows: { sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', xl: '0 25px 50px -12px rgb(0 0 0 / 0.25)' } }; this.sendMessage({ type: 'CUSTOMIZE_THEME', sessionId: this.sessionId, payload: { customTheme } }); } /** * Generate color palette from base color */ generateColorPalette(baseColor) { return { 50: this.adjustColor(baseColor, 95), 100: this.adjustColor(baseColor, 90), 200: this.adjustColor(baseColor, 80), 300: this.adjustColor(baseColor, 60), 400: this.adjustColor(baseColor, 40), 500: baseColor, 600: this.adjustColor(baseColor, -20), 700: this.adjustColor(baseColor, -40), 800: this.adjustColor(baseColor, -60), 900: this.adjustColor(baseColor, -80), 950: this.adjustColor(baseColor, -90) }; } /** * Adjust color lightness */ adjustColor(color, percent) { const num = parseInt(color.replace('#', ''), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) + amt; const G = (num >> 8 & 0x00FF) + amt; const B = (num & 0x0000FF) + amt; return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1); } /** * Import template */ async importTemplate(templateData) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.off('templateImported', handleResponse); reject(new Error('Import template timeout')); }, 30000); // 30 second timeout const handleResponse = (data) => { clearTimeout(timeout); this.off('templateImported', handleResponse); if (data.success) { const result = { success: true, templateId: templateData.id || 'unknown', workspaceId: data.workspaceId || 'unknown', templateName: templateData.name || 'Imported Template', tracksCount: templateData.tracks?.length || 0, elementsCount: templateData.tracks?.reduce((total, track) => total + (track.elements?.length || 0), 0) || 0, timestamp: new Date().toISOString() }; resolve(result); } else { const result = { success: false, templateId: templateData.id || 'unknown', workspaceId: '', templateName: templateData.name || 'Imported Template', tracksCount: templateData.tracks?.length || 0, elementsCount: templateData.tracks?.reduce((total, track) => total + (track.elements?.length || 0), 0) || 0, timestamp: new Date().toISOString(), error: data.error || 'Import failed' }; resolve(result); } }; // Listen for template import response this.on('templateImported', handleResponse); // Send import template message this.sendMessage({ type: 'IMPORT_TEMPLATE', sessionId: this.sessionId, payload: templateData }); }); } /** * Import template from assets array - Auto-generates template with video/audio track separation */ async importFromAssets(assets, options) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } if (!assets || assets.length === 0) { throw new Error('Assets array is required and cannot be empty'); } console.log('๐ŸŽฌ Generating template from assets:', assets.length, 'assets'); // Set default options const opts = { name: 'Generated Project', description: 'Auto-generated from assets', width: 1920, height: 1080, fps: 30, backgroundColor: '#000000', ...options }; // Calculate max duration in frames from all assets let maxDurationMs = 0; assets.forEach(asset => { if (asset.duration && asset.duration > maxDurationMs) { maxDurationMs = asset.duration; } }); // Default to 5 seconds if no duration found, convert to frames const maxDurationFrames = maxDurationMs > 0 ? Math.ceil(maxDurationMs / 1000 * opts.fps) : opts.fps * 5; console.log(`๐ŸŽฏ Max duration: ${maxDurationMs}ms = ${maxDurationFrames} frames at ${opts.fps}fps`); // Generate template structure const template = { id: this.generateUUID(), name: opts.name, description: opts.description, version: '1.0.0', settings: { width: opts.width, height: opts.height, fps: opts.fps, duration: maxDurationFrames, backgroundColor: opts.backgroundColor, quality: 'high', audioSampleRate: 48000, audioChannels: 2 }, tracks: this.generateTracksFromAssets(assets, opts), assets: { videos: [], images: [], audio: [], fonts: [], shapes: [], templates: [] }, animations: [], transitions: [], groups: [], metadata: { author: 'EMS Editor SDK', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: '1.0.0', license: 'MIT', tags: [], category: 'Generated', complexity: 'beginner', description: opts.description, requirements: [], compatibility: ['web', 'mobile'] } }; console.log('โœ… Generated template:', template); // Import the generated template and wait for confirmation return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.off('templateImported', handleResponse); reject(new Error('Import from assets timeout')); }, 30000); // 30 second timeout const handleResponse = (data) => { clearTimeout(timeout); this.off('templateImported', handleResponse); if (data.success) { const result = { success: true, templateId: template.id, workspaceId: data.workspaceId || 'unknown', templateName: template.name, tracksCount: template.tracks.length, elementsCount: template.tracks.reduce((total, track) => total + track.elements.length, 0), timestamp: new Date().toISOString() }; resolve(result); } else { const result = { success: false, templateId: template.id, workspaceId: '', templateName: template.name, tracksCount: template.tracks.length, elementsCount: template.tracks.reduce((total, track) => total + track.elements.length, 0), timestamp: new Date().toISOString(), error: data.error || 'Import failed' }; resolve(result); } }; // Listen for template import response this.on('templateImported', handleResponse); // Send import template message this.sendMessage({ type: 'IMPORT_TEMPLATE', sessionId: this.sessionId, payload: template }); }); } /** * Generate tracks from assets with proper video/audio separation */ generateTracksFromAssets(assets, options) { const tracks = []; let videoTrackIndex = 1; let audioTrackIndex = 1; let imageTrackIndex = 1; let currentPosition = 0; assets.forEach((asset, index) => { const elementId = this.generateElementId(); const durationFrames = asset.duration ? Math.ceil(asset.duration / 1000 * options.fps) : options.fps * 5; // 5 seconds default if (asset.type === 'video') { // Create linked video and audio tracks for video assets const videoElementId = `${elementId}-video`; const audioElementId = `${elementId}-audio`; // Video track const videoTrack = this.findOrCreateTrack(tracks, 'video', `Video Track ${videoTrackIndex}`); const videoElement = this.createVideoElement(asset, videoElementId, audioElementId, currentPosition, durationFrames, options); videoTrack.elements.push(videoElement); // Audio track const audioTrack = this.findOrCreateTrack(tracks, 'audio', `Audio Track ${audioTrackIndex}`); const audioElement = this.createAudioElement(asset, audioElementId, videoElementId, currentPosition, durationFrames, options); audioTrack.elements.push(audioElement); videoTrackIndex++; audioTrackIndex++; } else if (asset.type === 'audio') { // Audio-only track const audioTrack = this.findOrCreateTrack(tracks, 'audio', `Audio Track ${audioTrackIndex}`); const audioElement = this.createAudioOnlyElement(asset, elementId, currentPosition, durationFrames, options); audioTrack.elements.push(audioElement); audioTrackIndex++; } else if (asset.type === 'image') { // Image track const imageTrack = this.findOrCreateTrack(tracks, 'image', `Image Track ${imageTrackIndex}`); const imageElement = this.createImageElement(asset, elementId, currentPosition, durationFrames, options); imageTrack.elements.push(imageElement); imageTrackIndex++; } // Move position for next asset (sequential placement) currentPosition += durationFrames; }); // Set proper z-index for tracks tracks.forEach((track, index) => { track.zIndex = tracks.length - index; // Video on top, audio on bottom }); return tracks; } /** * Find existing track or create new one */ findOrCreateTrack(tracks, type, name) { let track = tracks.find(t => t.type === type && t.elements.length === 0); if (!track) { track = { id: `track-${Date.now()}-${type}`, name, type, locked: false, visible: true, muted: type === 'audio' ? false : undefined, volume: type === 'audio' ? 1 : undefined, zIndex: tracks.length + 1, elements: [] }; tracks.push(track); } return track; } /** * Create video element with linking */ createVideoElement(asset, videoId, audioId, startFrame, duration, options) { return { id: videoId, type: 'video', name: `${asset.name} (Video)`, startFrame, endFrame: startFrame + duration, layerIndex: 0, transform: this.calculateTransform(asset, options), properties: { src: asset.proxyUrl || asset.fileUrl, originalSrc: asset.fileUrl, muted: true, volume: 0, originalWidth: asset.width, originalHeight: asset.height, aspectRatio: asset.width && asset.height ? asset.width / asset.height : undefined, mamAssetId: asset.mamAssetId || asset.id, mamOriginalType: 'Video', isMAMAsset: !!asset.mamAssetId, fileUrl: asset.fileUrl, proxyUrl: asset.proxyUrl, isLinked: true, linkedElementId: audioId, linkType: 'video-audio', originalMediaUrl: asset.fileUrl, playbackMode: 'video-only' }, animations: [], filters: [], blendMode: 'normal', opacity: 1 }; } /** * Create audio element with linking */ createAudioElement(asset, audioId, videoId, startFrame, duration, options) { return { id: audioId, type: 'audio', name: `${asset.name} (Audio)`, startFrame, endFrame: startFrame + duration, layerIndex: 0, transform: this.calculateTransform(asset, options), properties: { src: asset.proxyUrl || asset.fileUrl, originalSrc: asset.fileUrl, originalWidth: asset.width, originalHeight: asset.height, aspectRatio: asset.width && asset.height ? asset.width / asset.height : undefined, mamAssetId: asset.mamAssetId || asset.id, mamOriginalType: 'Video', isMAMAsset: !!asset.mamAssetId, fileUrl: asset.fileUrl, proxyUrl: asset.proxyUrl, isLinked: true, linkedElementId: videoId, linkType: 'video-audio', originalMediaUrl: asset.fileUrl, playbackMode: 'audio-only' }, animations: [], filters: [], blendMode: 'normal', opacity: 1 }; } /** * Create audio-only element */ createAudioOnlyElement(asset, elementId, startFrame, duration, options) { return { id: elementId, type: 'audio', name: asset.name, startFrame, endFrame: startFrame + duration, layerIndex: 0, transform: this.calculateTransform(asset, options), properties: { src: asset.proxyUrl || asset.fileUrl, originalSrc: asset.fileUrl, mamAssetId: asset.mamAssetId || asset.id, mamOriginalType: 'Audio', isMAMAsset: !!asset.mamAssetId, fileUrl: asset.fileUrl, proxyUrl: asset.proxyUrl, isLinked: false, playbackMode: 'both' }, animations: [], filters: [], blendMode: 'normal', opacity: 1 }; } /** * Create image element */ createImageElement(asset, elementId, startFrame, duration, options) { return { id: elementId, type: 'image', name: asset.name, startFrame, endFrame: startFrame + duration, layerIndex: 0, transform: this.calculateTransform(asset, options), properties: { src: asset.proxyUrl || asset.fileUrl, originalSrc: asset.fileUrl, originalWidth: asset.width, originalHeight: asset.height, aspectRatio: asset.width && asset.height ? asset.width / asset.height : undefined, mamAssetId: asset.mamAssetId || asset.id, mamOriginalType: 'Image', isMAMAsset: !!asset.mamAssetId, fileUrl: asset.fileUrl, proxyUrl: asset.proxyUrl, isLinked: false }, animations: [], filters: [], blendMode: 'normal', opacity: 1 }; } /** * Calculate transform for asset positioning */ calculateTransform(asset, options) { const canvasWidth = options.width; const canvasHeight = options.height; if (asset.type === 'image' || asset.type === 'video') { // Use asset dimensions if available, otherwise default const assetWidth = asset.width || canvasWidth; const assetHeight = asset.height || canvasHeight; // Calculate contain fit (like CSS object-fit: contain) const canvasAspectRatio = canvasWidth / canvasHeight; const assetAspectRatio = assetWidth / assetHeight; let scaledWidth; let scaledHeight; if (assetAspectRatio > canvasAspectRatio) { // Asset is wider - scale based on width scaledWidth = canvasWidth; scaledHeight = scaledWidth / assetAspectRatio; } else { // Asset is taller - scale based on height scaledHeight = canvasHeight; scaledWidth = scaledHeight * assetAspectRatio; } // Center on canvas const x = (canvasWidth - scaledWidth) / 2; const y = (canvasHeight - scaledHeight) / 2; return { x: Math.round(x), y: Math.round(y), width: Math.round(scaledWidth), height: Math.round(scaledHeight), rotation: 0, scaleX: 1, scaleY: 1, skewX: 0, skewY: 0, originX: 0.5, originY: 0.5 }; } // Default transform for audio return { x: 0, y: 0, width: canvasWidth, height: canvasHeight, rotation: 0, scaleX: 1, scaleY: 1, skewX: 0, skewY: 0, originX: 0.5, originY: 0.5 }; } /** * Generate UUID */ generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * Generate element ID */ generateElementId() { return `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Start render process */ async startRender(renderOptions) { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'START_RENDER', sessionId: this.sessionId, payload: renderOptions }); } /** * Clear all workspaces/tabs in the editor */ async clearWorkspaces() { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } this.sendMessage({ type: 'CLEAR_WORKSPACES', sessionId: this.sessionId, payload: {} }); } /** * Get list of all open workspaces in the editor */ async getWorkspacesList() { if (!this.ready) { throw new Error('Editor not ready. Wait for ready event.'); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.off('workspacesListReceived', handleResponse); reject(new Error('Get workspaces list timeout')); }, 10000); // 10 second timeout const handleResponse = (data) => { clearTimeout(timeout); this.off('workspacesListReceived', handleResponse); if (data.success) { resolve(data.workspaces || []); } else { reject(new Error(data.error || 'Failed to get workspaces list')); } }; // Listen for response this.on('workspacesListReceived', handleResponse); // Send request this.sendMessage({ type: 'GET_WORKSPACES_LIST', sessionId: this.sessionId, payload: {} }); }); } /** * MAM Upload Event Handlers */ /** * Track MAM upload progress for a specific job */ trackMAMUpload(jobId, callbacks = {}) { const trackingId = `mam-upload-${jobId}-${Date.now()}`; // Default callbacks const { onProgress = (progress) => console.log(`๐Ÿ“ค MAM Upload Progress: ${progress.progress}%`), onStageChange = (stage, data) => console.log(`๐Ÿ“ค MAM Upload Stage: ${stage}`, data), onComplete = (result) => console.log('โœ… MAM Upload Complete:', result), onError = (error) => console.error('โŒ MAM Upload Error:', error) } = callbacks; // Track upload events const handleUploadEvent = (data) => { if (data.jobId === jobId) { onProgress(data); onStageChange(data.stage, data); } }; const handleUploadComplete = (data) => { if (data.jobId === jobId) { onComplete(data); this.off('mamUploadEvent', handleUploadEvent); this.off('mamUploadComplete', handleUploadComplete); this.off('renderComplete', handleRenderComplete); } }; const handleRenderComplete = (data) => { if (data.jobId === jobId && data.mamUploadError) { onError(data.mamUploadError); this.off('mamUploadEvent', handleUploadEvent); this.off('mamUploadComplete', handleUploadComplete); this.off('renderComplete', handleRenderComplete); } }; this.on('mamUploadEvent', handleUploadEvent); this.on('mamUploadComplete', handleUploadComplete); this.on('renderComplete', handleRenderComplete); return { trackingId, stop: () => { this.off('mamUploadEvent', handleUploadEvent); this.off('mamUploadComplete', handleUploadComplete); this.off('renderComplete', handleRenderComplete); } }; } /** * Start render with MAM upload tracking */ async startRenderWithMAMUpload(renderOptions, mamUploadCallbacks = {}) { console.log('๐ŸŽฌ Starting render with MAM upload tracking...'); // Start the render await this.startRender(renderOptions); // Wait for render to start and get job ID from render progress return new Promise((resolve) => { const handleRenderProgress = (data) => { if (data.jobId) { // Set up MAM upload tracking const tracker = this.trackMAMUpload(data.jobId, mamUploadCallbacks); // Remove this temporary listener this.off('renderProgress', handleRenderProgress); resolve({ jobId: data.jobId, tracker }); } }; this.on('renderProgress', handleRenderProgress); }); } // Status Methods isConnected() { return this.connected; } isReady() { return this.ready; } getSessionId() { return this.sessionId; } // Event System on(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } this.eventListeners.get(event).add(listener); } off(event, listener) { const listeners = this.eventListeners.get(event); if (listeners) { listeners.delete(listener); } } once(event, listener) { const onceWrapper = (...args) => { listener(...args); this.off(event, onceWrapper); }; this.on(event, onceWrapper); } emit(event, data) { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(listener => { try { listener(data); } catch (error) { console.error(`Error in ${event} listener:`, error); } }); } } // Utility Methods sendMessage(message) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); console.log('๐Ÿ“ค Sent:', message.type); } else { console.warn('WebSocket not connected, cannot send message:', message.type); } } /** * Disconnect and cleanup */ destroy() { if (this.ws) { this.ws.close(); this.ws = null; } this.connected = false; this.ready = false; this.eventListeners.clear(); console.log('๐Ÿงน SDK destroyed'); } } exports.EMSEditor = EMSEditor; exports.default = EMSEditor; //# sourceMappingURL=index.js.map