UNPKG

@opensubtitles/video-metadata-extractor

Version:

A comprehensive NPM package for video metadata extraction and subtitle processing using FFmpeg WASM. Supports metadata extraction, individual subtitle extraction, batch subtitle extraction with ZIP downloads, and memory-safe processing of files of any siz

262 lines 12.7 kB
/** * FFmpeg WASM Cache Utility * Caches FFmpeg core files in IndexedDB to avoid re-downloading on page reload */ const CACHE_DB_NAME = 'ffmpeg-wasm-cache'; const CACHE_DB_VERSION = 1; const CACHE_STORE_NAME = 'files'; const CACHE_EXPIRY_DAYS = 7; // Cache expires after 7 days const FFMPEG_VERSION = '0.12.6'; // FFmpeg version for cache invalidation class FFmpegCache { constructor() { this.db = null; } async init() { console.log(`[FFmpeg Cache] Initializing cache database: ${CACHE_DB_NAME} v${CACHE_DB_VERSION}`); return new Promise((resolve, reject) => { const request = indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION); request.onerror = () => { console.error(`[FFmpeg Cache] Failed to open database:`, request.error); reject(request.error); }; request.onsuccess = () => { this.db = request.result; console.log(`[FFmpeg Cache] Database opened successfully`); console.log(`[FFmpeg Cache] Object stores: ${Array.from(this.db.objectStoreNames).join(', ')}`); resolve(); }; request.onupgradeneeded = (event) => { console.log(`[FFmpeg Cache] Database upgrade needed`); const db = event.target.result; if (!db.objectStoreNames.contains(CACHE_STORE_NAME)) { console.log(`[FFmpeg Cache] Creating object store: ${CACHE_STORE_NAME}`); const store = db.createObjectStore(CACHE_STORE_NAME, { keyPath: 'url' }); store.createIndex('timestamp', 'timestamp', { unique: false }); console.log(`[FFmpeg Cache] Object store created with timestamp index`); } else { console.log(`[FFmpeg Cache] Object store ${CACHE_STORE_NAME} already exists`); } }; }); } async get(url) { if (!this.db) await this.init(); console.log(`[FFmpeg Cache] Looking for cached file: ${url}`); return new Promise((resolve, reject) => { const transaction = this.db.transaction([CACHE_STORE_NAME], 'readonly'); const store = transaction.objectStore(CACHE_STORE_NAME); const request = store.get(url); request.onerror = () => { console.error(`[FFmpeg Cache] Error getting ${url}:`, request.error); reject(request.error); }; request.onsuccess = () => { const result = request.result; console.log(`[FFmpeg Cache] Get request completed for ${url}`); console.log(`[FFmpeg Cache] Result found: ${!!result}`); if (!result) { console.log(`[FFmpeg Cache] Cache miss for ${url} - not found in database`); // Debug: List all cached URLs to see what's in the database const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { const allFiles = getAllRequest.result; console.log(`[FFmpeg Cache] DEBUG: All cached URLs in database:`); allFiles.forEach((file, i) => { console.log(` ${i + 1}. "${file.url}" (${Math.round(file.data.byteLength / 1024 / 1024)}MB, ${file.version})`); }); console.log(`[FFmpeg Cache] Looking for: "${url}"`); console.log(`[FFmpeg Cache] Exact matches: ${allFiles.filter(f => f.url === url).length}`); }; resolve(null); return; } // Check if cache is expired or version changed const now = Date.now(); const expiryTime = CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000; const age = now - result.timestamp; console.log(`[FFmpeg Cache] Found cached file for ${url}:`); console.log(` - Size: ${Math.round(result.data.byteLength / 1024 / 1024)}MB`); console.log(` - Age: ${Math.round(age / 1000 / 60)} minutes`); console.log(` - Version: ${result.version} (current: ${FFMPEG_VERSION})`); console.log(` - Expired: ${age > expiryTime ? 'YES' : 'NO'}`); if (now - result.timestamp > expiryTime || result.version !== FFMPEG_VERSION) { // Cache expired or version changed, remove it console.log(`[FFmpeg Cache] Cache expired or version mismatch, removing...`); this.remove(url); resolve(null); return; } console.log(`[FFmpeg Cache] Cache hit for ${url} - returning ${Math.round(result.data.byteLength / 1024 / 1024)}MB`); resolve(result.data); }; }); } async set(url, data) { if (!this.db) { console.log(`[FFmpeg Cache] Database not initialized, initializing for set operation`); await this.init(); } console.log(`[FFmpeg Cache] Attempting to cache ${url}`); console.log(`[FFmpeg Cache] Data size: ${Math.round(data.byteLength / 1024 / 1024)}MB`); console.log(`[FFmpeg Cache] Version: ${FFMPEG_VERSION}`); console.log(`[FFmpeg Cache] Timestamp: ${new Date().toISOString()}`); const cachedFile = { url, data, timestamp: Date.now(), version: FFMPEG_VERSION }; return new Promise((resolve, reject) => { console.log(`[FFmpeg Cache] Creating transaction for ${CACHE_STORE_NAME}`); const transaction = this.db.transaction([CACHE_STORE_NAME], 'readwrite'); transaction.onerror = () => { console.error(`[FFmpeg Cache] Transaction error for ${url}:`, transaction.error); reject(transaction.error); }; transaction.oncomplete = () => { console.log(`[FFmpeg Cache] Transaction completed successfully for ${url}`); }; const store = transaction.objectStore(CACHE_STORE_NAME); console.log(`[FFmpeg Cache] Putting data into store for ${url}`); const request = store.put(cachedFile); request.onerror = () => { console.error(`[FFmpeg Cache] Put request error for ${url}:`, request.error); reject(request.error); }; request.onsuccess = () => { console.log(`[FFmpeg Cache] ✅ Successfully cached ${url} (${Math.round(data.byteLength / 1024 / 1024)}MB)`); console.log(`[FFmpeg Cache] Cache key: ${url}`); resolve(); }; }); } async remove(url) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([CACHE_STORE_NAME], 'readwrite'); const store = transaction.objectStore(CACHE_STORE_NAME); const request = store.delete(url); request.onerror = () => reject(request.error); request.onsuccess = () => { console.log(`[FFmpeg Cache] Removed ${url} from cache`); resolve(); }; }); } async clear() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([CACHE_STORE_NAME], 'readwrite'); const store = transaction.objectStore(CACHE_STORE_NAME); const request = store.clear(); request.onerror = () => reject(request.error); request.onsuccess = () => { console.log('[FFmpeg Cache] Cache cleared'); resolve(); }; }); } async getCacheSize() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([CACHE_STORE_NAME], 'readonly'); const store = transaction.objectStore(CACHE_STORE_NAME); const request = store.getAll(); request.onerror = () => reject(request.error); request.onsuccess = () => { const files = request.result; const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0); resolve(totalSize); }; }); } } // Create a cached version of toBlobURL export async function cachedToBlobURL(url, mimeType) { console.log(`[FFmpeg Cache] ===== CACHED TO BLOB URL START =====`); console.log(`[FFmpeg Cache] URL: ${url}`); console.log(`[FFmpeg Cache] MIME type: ${mimeType}`); console.log(`[FFmpeg Cache] Call stack:`, new Error().stack?.split('\n').slice(1, 4).join('\n')); const cache = new FFmpegCache(); try { // Try to get from cache first console.log(`[FFmpeg Cache] Checking cache for ${url}...`); const cachedData = await cache.get(url); if (cachedData) { console.log(`[FFmpeg Cache] ✅ CACHE HIT! Using cached file for ${url}`); console.log(`[FFmpeg Cache] Cached data size: ${Math.round(cachedData.byteLength / 1024 / 1024)}MB`); const blob = new Blob([cachedData], { type: mimeType }); const blobUrl = URL.createObjectURL(blob); console.log(`[FFmpeg Cache] Created blob URL from cache: ${blobUrl.substring(0, 50)}...`); console.log(`[FFmpeg Cache] ===== CACHED TO BLOB URL END (FROM CACHE) =====`); return blobUrl; } // Not in cache, fetch it console.log(`[FFmpeg Cache] ❌ Cache miss for ${url} - fetching from network`); console.log(`[FFmpeg Cache] Fetching: ${url}`); const fetchStart = performance.now(); const response = await fetch(url); const fetchEnd = performance.now(); console.log(`[FFmpeg Cache] Fetch completed in ${Math.round(fetchEnd - fetchStart)}ms`); console.log(`[FFmpeg Cache] Response status: ${response.status} ${response.statusText}`); console.log(`[FFmpeg Cache] Response size: ${response.headers.get('content-length') || 'unknown'} bytes`); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); console.log(`[FFmpeg Cache] Downloaded ${Math.round(arrayBuffer.byteLength / 1024 / 1024)}MB for ${url}`); // Cache it for next time console.log(`[FFmpeg Cache] Caching ${url} for future use...`); await cache.set(url, arrayBuffer); console.log(`[FFmpeg Cache] Successfully cached ${url}`); // Return blob URL const blob = new Blob([arrayBuffer], { type: mimeType }); const blobUrl = URL.createObjectURL(blob); console.log(`[FFmpeg Cache] Created blob URL: ${blobUrl.substring(0, 50)}...`); console.log(`[FFmpeg Cache] ===== CACHED TO BLOB URL END =====`); return blobUrl; } catch (error) { console.error(`[FFmpeg Cache] Error with cached fetch for ${url}:`, error); // Fallback to regular fetch if cache fails console.log(`[FFmpeg Cache] Falling back to direct fetch for ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); const blob = new Blob([arrayBuffer], { type: mimeType }); return URL.createObjectURL(blob); } } // Utility function to get cache info export async function getCacheInfo() { const cache = new FFmpegCache(); try { await cache.init(); const size = await cache.getCacheSize(); const sizeFormatted = size > 0 ? `${Math.round(size / 1024 / 1024)}MB` : '0MB'; return { size, sizeFormatted, files: size > 0 ? 2 : 0 // Assume 2 files if there's any data }; } catch (error) { console.error('[FFmpeg Cache] Error getting cache info:', error); return { size: 0, sizeFormatted: '0MB', files: 0 }; } } // Utility function to clear cache export async function clearFFmpegCache() { const cache = new FFmpegCache(); await cache.clear(); } export default FFmpegCache; //# sourceMappingURL=ffmpegCache.js.map