UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

1,003 lines 44.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserPool = void 0; const playwright_1 = require("playwright"); const events_1 = require("events"); const error_recovery_1 = require("./error-recovery"); /** * Browser pool manager that handles browser instance lifecycle and resource monitoring */ class BrowserPool extends events_1.EventEmitter { constructor(config) { super(); this.instances = new Map(); this.availableInstances = new Set(); this.isShuttingDown = false; this.metricsHistory = new Map(); this.config = config; this.errorRecovery = new error_recovery_1.ErrorRecoveryManager({ failureThreshold: 3, recoveryTimeout: 30000, successThreshold: 2, monitoringWindow: 300000 }); this.setupErrorRecoveryEvents(); this.startResourceMonitoring(); } /** * Initialize the browser pool with minimum instances */ async initialize() { const promises = []; for (let i = 0; i < this.config.minInstances; i++) { promises.push(this.createBrowserInstance()); } await Promise.all(promises); this.emit('initialized', { instanceCount: this.instances.size }); } /** * Acquire a browser instance from the pool */ async acquireInstance() { if (this.isShuttingDown) { throw new Error('Browser pool is shutting down'); } // Try to get an available instance first, checking circuit breaker state for (const availableId of this.availableInstances) { if (this.errorRecovery.canUseInstance(availableId)) { const instance = this.instances.get(availableId); this.availableInstances.delete(availableId); instance.isActive = true; instance.lastUsed = new Date(); // Record successful acquisition this.errorRecovery.recordSuccess(availableId); this.emit('instanceAcquired', { instanceId: availableId }); return instance; } } // Create new instance if under limit if (this.instances.size < this.config.maxInstances) { const instance = await this.createBrowserInstance(); instance.isActive = true; this.emit('instanceAcquired', { instanceId: instance.id }); return instance; } // Wait for an instance to become available return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for available browser instance')); }, 30000); // 30 second timeout const onInstanceReleased = () => { clearTimeout(timeout); this.removeListener('instanceReleased', onInstanceReleased); // Try to get an available instance directly instead of recursive call for (const availableId of this.availableInstances) { if (this.errorRecovery.canUseInstance(availableId)) { const instance = this.instances.get(availableId); this.availableInstances.delete(availableId); instance.isActive = true; instance.lastUsed = new Date(); // Record successful acquisition this.errorRecovery.recordSuccess(availableId); this.emit('instanceAcquired', { instanceId: availableId }); resolve(instance); return; } } // If no available instances, wait for another release this.on('instanceReleased', onInstanceReleased); }; this.on('instanceReleased', onInstanceReleased); }); } /** * Release a browser instance back to the pool */ async releaseInstance(instanceId) { const instance = this.instances.get(instanceId); if (!instance) { throw new Error(`Browser instance ${instanceId} not found`); } instance.isActive = false; instance.lastUsed = new Date(); // Perform comprehensive memory cleanup try { await this.performMemoryCleanup(instance); } catch (error) { // If cleanup fails, destroy the instance await this.destroyInstance(instanceId); return; } this.availableInstances.add(instanceId); this.emit('instanceReleased', { instanceId }); } /** * Get current metrics for all browser instances (including recently disconnected) */ getMetrics() { const currentMetrics = Array.from(this.instances.values()).map(instance => ({ ...instance.metrics, uptime: (Date.now() - instance.createdAt.getTime()) / 1000 })); // Include metrics from recently disconnected instances (within last 30 seconds) const recentDisconnectedMetrics = []; const cutoffTime = Date.now() - 30000; // 30 seconds ago for (const [instanceId, historyEntry] of this.metricsHistory) { if (historyEntry.disconnectedAt.getTime() > cutoffTime) { recentDisconnectedMetrics.push(historyEntry.metrics); } else { // Clean up old entries this.metricsHistory.delete(instanceId); } } return [...currentMetrics, ...recentDisconnectedMetrics]; } /** * Get pool status information */ getPoolStatus() { return { totalInstances: this.instances.size, availableInstances: this.availableInstances.size, activeInstances: this.instances.size - this.availableInstances.size, maxInstances: this.config.maxInstances, resourceLimits: this.config.resourceLimits }; } /** * Shutdown the browser pool and cleanup all instances */ async shutdown() { this.isShuttingDown = true; if (this.resourceMonitorInterval) { clearInterval(this.resourceMonitorInterval); } // Shutdown error recovery manager this.errorRecovery.shutdown(); const shutdownPromises = Array.from(this.instances.keys()).map(id => this.destroyInstance(id)); await Promise.all(shutdownPromises); // Clean up any remaining DRM profiles (safety cleanup) await this.cleanupAllDrmProfiles(); this.emit('shutdown'); } /** * Get error recovery statistics */ getErrorRecoveryStats() { return this.errorRecovery.getRecoveryStats(); } /** * Get the appropriate browser launcher and options based on browser type */ async getBrowserLauncherAndOptions(browserType) { if (browserType === 'chrome') { try { // For Chrome, we use the chromium launcher but specify Chrome's executable path // This allows us to use the full Chrome browser with DRM support // Try to find Chrome executable path let chromeExecutablePath; // Platform-specific Chrome paths const platform = process.platform; if (platform === 'darwin') { chromeExecutablePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; } else if (platform === 'win32') { chromeExecutablePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; } else if (platform === 'linux') { chromeExecutablePath = '/usr/bin/google-chrome'; } return { launcher: playwright_1.chromium, executablePath: chromeExecutablePath }; } catch (error) { console.warn('Chrome browser not available, falling back to Chromium. DRM functionality may be limited.'); return { launcher: playwright_1.chromium }; } } return { launcher: playwright_1.chromium }; } /** * Setup error recovery event handlers */ setupErrorRecoveryEvents() { this.errorRecovery.on('circuit-breaker-opened', ({ instanceId }) => { this.emit('circuitBreakerOpened', { instanceId }); }); this.errorRecovery.on('circuit-breaker-closed', ({ instanceId }) => { this.emit('circuitBreakerClosed', { instanceId }); }); this.errorRecovery.on('instance-blacklisted', ({ instanceId, reason }) => { this.emit('instanceBlacklisted', { instanceId, reason }); // Remove blacklisted instance from available pool this.availableInstances.delete(instanceId); }); this.errorRecovery.on('restart-attempted', ({ instanceId, success, error }) => { this.emit('instanceRestartAttempted', { instanceId, success, error }); }); this.errorRecovery.on('error-logged', (errorLog) => { this.emit('errorLogged', errorLog); }); } /** * Create a new browser instance */ async createBrowserInstance() { const instanceId = `browser-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Determine browser type and headless mode based on DRM configuration const isDrmEnabled = !!this.config.drmConfig; const browserType = this.config.browserOptions?.browserType || (isDrmEnabled ? 'chrome' : 'chromium'); const headlessMode = isDrmEnabled ? false : (this.config.browserOptions?.headless ?? true); // Get the appropriate browser launcher and options const { launcher: browserLauncher, executablePath } = await this.getBrowserLauncherAndOptions(browserType); const browserOptions = { headless: headlessMode, ...(executablePath && { executablePath }), args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', '--disable-extensions', '--disable-plugins', '--disable-default-apps', '--disable-sync', '--no-first-run', '--no-default-browser-check', '--disable-gpu', // Additional stability flags '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-background-networking', '--disable-client-side-phishing-detection', '--disable-hang-monitor', '--disable-popup-blocking', '--disable-prompt-on-repost', '--metrics-recording-only', '--safebrowsing-disable-auto-update', '--enable-automation', '--password-store=basic', '--use-mock-keychain', // DRM-specific flags for Chrome ...(browserType === 'chrome' ? [ '--enable-widevine-cdm', '--disable-features=VizDisplayCompositor', // Additional DRM-specific flags '--enable-features=VaapiVideoDecoder', '--disable-component-update', // Prevent Widevine updates during testing '--allow-running-insecure-content', // For mixed content scenarios '--disable-web-security', // Already included above but important for DRM '--autoplay-policy=no-user-gesture-required', // Already in config but moved here for DRM context '--enable-logging=stderr', // Enable logging for DRM debugging '--log-level=0', // Verbose logging for troubleshooting // Native Chrome DRM permissions (bypass Playwright limitations) '--disable-features=UserMediaScreenCapturing', // Allow media access '--use-fake-ui-for-media-stream', // Auto-grant media permissions '--disable-background-media-suspend', // Keep DRM active '--disable-backgrounding-occluded-windows', // Prevent DRM suspension '--enable-experimental-web-platform-features', // Enable latest DRM features '--ignore-certificate-errors-spki-list', // For DRM certificate validation '--ignore-ssl-errors', // For DRM HTTPS requirements '--allow-running-insecure-content', // For mixed DRM content '--disable-site-isolation-trials' // For DRM cross-origin access ] : []), ...(this.config.browserOptions?.args || []) ] }; try { let browser; let context; let page; if (isDrmEnabled) { // For DRM, use launchPersistentContext with a temporary profile const persistentContextOptions = { ...browserOptions, userDataDir: `/tmp/chrome-drm-profile-${instanceId}`, viewport: { width: 1920, height: 1080 }, screen: { width: 1920, height: 1080 }, deviceScaleFactor: 1, ignoreHTTPSErrors: true, extraHTTPHeaders: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }, permissions: [ 'camera', 'microphone', 'geolocation', 'notifications' ] }; try { context = await browserLauncher.launchPersistentContext(`/tmp/chrome-drm-profile-${instanceId}`, persistentContextOptions); browser = context; // In persistent context, context acts as browser page = await context.newPage(); } catch (error) { // Fallback to regular browser launch if persistent context fails console.warn('DRM persistent context failed, falling back to regular browser:', error instanceof Error ? error.message : 'Unknown error'); browser = await browserLauncher.launch(browserOptions); context = await browser.newContext({ viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true }); page = await context.newPage(); } } else { // Regular browser launch for non-DRM scenarios browser = await browserLauncher.launch(browserOptions); context = await browser.newContext({ viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true }); page = await context.newPage(); } // Log browser type and mode for debugging this.emit('browserInstanceCreated', { instanceId, browserType, headless: headlessMode, isDrmEnabled }); // Initialize localStorage if configured if (this.config.localStorage && this.config.localStorage.length > 0) { await this.initializeLocalStorage(page); } // Setup and verify DRM capabilities if DRM is enabled if (isDrmEnabled) { await this.setupDrmPermissions(context, instanceId); await this.verifyDrmCapabilities(page, instanceId); } const instance = { id: instanceId, browser, context, page, createdAt: new Date(), lastUsed: new Date(), isActive: false, metrics: { instanceId, memoryUsage: 0, cpuUsage: 0, requestCount: 0, errorCount: 0, uptime: 0 } }; this.instances.set(instanceId, instance); this.availableInstances.add(instanceId); // Set up error handling browser.on('disconnected', () => { this.handleBrowserDisconnect(instanceId); }); this.emit('instanceCreated', { instanceId }); return instance; } catch (error) { this.emit('instanceCreationFailed', { instanceId, error }); throw error; } } /** * Destroy a browser instance and clean up resources */ async destroyInstance(instanceId) { const instance = this.instances.get(instanceId); if (!instance) { return; } try { await instance.context.close(); await instance.browser.close(); } catch (error) { // Ignore cleanup errors during shutdown } // Clean up temporary DRM profile if it exists await this.cleanupDrmProfile(instanceId); this.instances.delete(instanceId); this.availableInstances.delete(instanceId); this.emit('instanceDestroyed', { instanceId }); } /** * Handle browser disconnect events */ handleBrowserDisconnect(instanceId) { const instance = this.instances.get(instanceId); const uptime = instance ? (Date.now() - instance.createdAt.getTime()) / 1000 : 0; // Don't treat disconnections during shutdown as errors if (this.isShuttingDown) { // This is expected during shutdown - just clean up if (instance) { this.metricsHistory.set(instanceId, { metrics: { ...instance.metrics, uptime: (Date.now() - instance.createdAt.getTime()) / 1000 }, disconnectedAt: new Date() }); } this.instances.delete(instanceId); this.availableInstances.delete(instanceId); this.emit('instanceDisconnected', { instanceId }); return; } // Only treat as unexpected error if not shutting down const error = new Error('Browser instance disconnected unexpectedly'); // Record the failure only for unexpected disconnections this.errorRecovery.recordFailure(instanceId, error, { event: 'browser-disconnect', timestamp: new Date(), uptime }); // Store metrics in history before removing the instance if (instance) { this.metricsHistory.set(instanceId, { metrics: { ...instance.metrics, uptime: (Date.now() - instance.createdAt.getTime()) / 1000 }, disconnectedAt: new Date() }); } // Clean up the disconnected instance this.instances.delete(instanceId); this.availableInstances.delete(instanceId); this.emit('instanceDisconnected', { instanceId }); // Attempt automatic restart if conditions are met this.attemptInstanceRestart(instanceId, error); // Ensure we maintain minimum pool size this.ensureMinimumPoolSize(); } /** * Ensure the pool maintains minimum instance count */ async ensureMinimumPoolSize() { if (this.isShuttingDown) { return; } const currentCount = this.instances.size; const needed = this.config.minInstances - currentCount; if (needed > 0) { const promises = []; for (let i = 0; i < needed; i++) { promises.push(this.createBrowserInstance().catch(error => { // Log error but don't fail the entire operation this.emit('instanceCreationFailed', { instanceId: `recovery-${Date.now()}`, error }); })); } await Promise.all(promises); } } /** * Attempt to restart a failed browser instance */ async attemptInstanceRestart(instanceId, originalError) { // Check if restart should be attempted if (!this.errorRecovery.shouldRestartInstance(instanceId) || this.isShuttingDown) { return; } try { // Create a new instance to replace the failed one const newInstance = await this.createBrowserInstance(); // Record successful restart this.errorRecovery.recordRestartAttempt(instanceId, true); this.emit('instanceRestarted', { originalInstanceId: instanceId, newInstanceId: newInstance.id }); } catch (restartError) { // Record failed restart attempt this.errorRecovery.recordRestartAttempt(instanceId, false, restartError); this.emit('instanceRestartFailed', { instanceId, originalError, restartError: restartError }); } } /** * Start monitoring resource usage of browser instances */ startResourceMonitoring() { this.resourceMonitorInterval = setInterval(async () => { await this.updateResourceMetrics(); await this.enforceResourceLimits(); }, 5000); // Monitor every 5 seconds } /** * Update resource metrics for all browser instances */ async updateResourceMetrics() { for (const [instanceId, instance] of this.instances) { try { // Get memory usage from browser process const memoryInfo = await this.getBrowserMemoryUsage(instance.browser); instance.metrics.memoryUsage = memoryInfo; // CPU usage would require additional system monitoring // For now, we'll estimate based on activity instance.metrics.cpuUsage = instance.isActive ? 15 : 5; // Rough estimate // Check if instance exceeds memory limits if (memoryInfo > this.config.resourceLimits.maxMemoryPerInstance) { this.emit('resourceLimitExceeded', { instanceId, type: 'memory', usage: memoryInfo, limit: this.config.resourceLimits.maxMemoryPerInstance }); } } catch (error) { instance.metrics.errorCount++; this.emit('metricsUpdateFailed', { instanceId, error }); } } } /** * Get memory usage for a browser instance */ async getBrowserMemoryUsage(browser) { try { // This is a simplified approach - in production you might want to use // system monitoring tools or browser CDP for more accurate metrics const contexts = browser.contexts(); let totalMemory = 50; // Base browser memory estimate in MB for (const context of contexts) { const pages = context.pages(); totalMemory += pages.length * 20; // Estimate 20MB per page } return totalMemory; } catch { return 50; // Return base memory instead of 0 } } /** * Enforce resource limits by destroying instances that exceed limits */ async enforceResourceLimits() { const instancesToDestroy = []; const instancesToCleanup = []; for (const [instanceId, instance] of this.instances) { const { memoryUsage, cpuUsage } = instance.metrics; if (memoryUsage > this.config.resourceLimits.maxMemoryPerInstance || cpuUsage > this.config.resourceLimits.maxCpuPercentage) { if (!instance.isActive) { // If memory usage is critically high, destroy the instance if (memoryUsage > this.config.resourceLimits.maxMemoryPerInstance * 1.5) { instancesToDestroy.push(instanceId); } else { // Otherwise, try aggressive cleanup first instancesToCleanup.push(instanceId); } } else { // For active instances, emit warning but don't destroy this.emit('resourceLimitWarning', { instanceId, type: memoryUsage > this.config.resourceLimits.maxMemoryPerInstance ? 'memory' : 'cpu', usage: memoryUsage > this.config.resourceLimits.maxMemoryPerInstance ? memoryUsage : cpuUsage, limit: memoryUsage > this.config.resourceLimits.maxMemoryPerInstance ? this.config.resourceLimits.maxMemoryPerInstance : this.config.resourceLimits.maxCpuPercentage, isActive: true }); } } } // Try aggressive cleanup first for (const instanceId of instancesToCleanup) { try { const instance = this.instances.get(instanceId); if (instance) { await this.performAggressiveMemoryCleanup(instance); this.emit('instanceCleanedForResourceLimit', { instanceId }); } } catch (error) { // If cleanup fails, add to destroy list instancesToDestroy.push(instanceId); } } // Destroy instances that exceed limits or failed cleanup for (const instanceId of instancesToDestroy) { await this.destroyInstance(instanceId); this.emit('instanceDestroyedForResourceLimit', { instanceId }); } } /** * Perform comprehensive memory cleanup on browser instance */ async performMemoryCleanup(instance) { // Navigate to blank page to clear current page resources await instance.page.goto('about:blank'); // Clear browser context data await instance.context.clearCookies(); await instance.context.clearPermissions(); // Clear storage data try { await instance.page.evaluate(() => { // Clear localStorage if (typeof globalThis.localStorage !== 'undefined') { globalThis.localStorage.clear(); } // Clear sessionStorage if (typeof globalThis.sessionStorage !== 'undefined') { globalThis.sessionStorage.clear(); } // Clear IndexedDB if (typeof globalThis.indexedDB !== 'undefined') { globalThis.indexedDB.databases?.().then((databases) => { databases.forEach((db) => { if (db.name) { globalThis.indexedDB.deleteDatabase(db.name); } }); }); } }); } catch (error) { // Ignore storage cleanup errors } // Reset request count for metrics instance.metrics.requestCount = 0; } /** * Perform aggressive memory cleanup for instances approaching limits */ async performAggressiveMemoryCleanup(instance) { // Perform standard cleanup first await this.performMemoryCleanup(instance); // Force garbage collection if available try { await instance.page.evaluate(() => { // Force garbage collection in browser context if (typeof globalThis.window !== 'undefined' && globalThis.window.gc) { globalThis.window.gc(); } }); } catch (error) { // Ignore GC errors } // Close and recreate the page to free up resources try { await instance.page.close(); instance.page = await instance.context.newPage(); await instance.page.goto('about:blank'); } catch (error) { throw new Error(`Failed to recreate page during aggressive cleanup: ${error}`); } } /** * Get detailed resource usage statistics */ getResourceUsageStats() { const instances = Array.from(this.instances.values()); const totalMemory = instances.reduce((sum, instance) => sum + instance.metrics.memoryUsage, 0); const averageMemory = instances.length > 0 ? totalMemory / instances.length : 0; const totalCpu = instances.reduce((sum, instance) => sum + instance.metrics.cpuUsage, 0); const averageCpu = instances.length > 0 ? totalCpu / instances.length : 0; const memoryLimit = this.config.resourceLimits.maxMemoryPerInstance; const cpuLimit = this.config.resourceLimits.maxCpuPercentage; const instancesNearMemoryLimit = instances.filter(instance => instance.metrics.memoryUsage > memoryLimit * 0.8).length; const instancesNearCpuLimit = instances.filter(instance => instance.metrics.cpuUsage > cpuLimit * 0.8).length; return { totalInstances: instances.length, activeInstances: instances.filter(instance => instance.isActive).length, totalMemoryUsage: totalMemory, averageMemoryUsage: averageMemory, totalCpuUsage: totalCpu, averageCpuUsage: averageCpu, memoryUtilization: instances.length > 0 ? (totalMemory / (instances.length * memoryLimit)) * 100 : 0, cpuUtilization: (averageCpu / cpuLimit) * 100, instancesNearMemoryLimit, instancesNearCpuLimit, resourceLimits: this.config.resourceLimits }; } /** * Force cleanup of idle instances to free resources */ async cleanupIdleInstances(maxIdleTime = 300000) { const now = Date.now(); const instancesToCleanup = []; // Find idle instances that can be cleaned up for (const [instanceId, instance] of this.instances) { if (!instance.isActive && (now - instance.lastUsed.getTime()) > maxIdleTime) { instancesToCleanup.push(instanceId); } } // Only cleanup instances if we have more than minimum const maxToCleanup = Math.max(0, this.instances.size - this.config.minInstances); const actualCleanupCount = Math.min(instancesToCleanup.length, maxToCleanup); // Cleanup idle instances (up to the limit) for (let i = 0; i < actualCleanupCount; i++) { const instanceId = instancesToCleanup[i]; await this.destroyInstance(instanceId); this.emit('instanceCleanedForIdle', { instanceId }); } return actualCleanupCount; } /** * Setup DRM permissions using Chrome DevTools Protocol */ async setupDrmPermissions(context, instanceId) { try { // Use Chrome DevTools Protocol to enable DRM permissions const cdpSession = await context.newCDPSession(await context.pages()[0] || await context.newPage()); // Enable DRM-related domains await cdpSession.send('Browser.grantPermissions', { permissions: [ 'protectedMediaIdentifier', 'audioCapture', 'videoCapture', 'displayCapture' ] }); // Set DRM-friendly browser settings await cdpSession.send('Browser.setPermission', { permission: { name: 'protectedMediaIdentifier' }, setting: 'granted' }); this.emit('drmPermissionsSetup', { instanceId, success: true, timestamp: new Date() }); } catch (error) { // Log warning but don't fail - browser flags should handle DRM console.warn(`DRM permissions setup failed for instance ${instanceId}, relying on browser flags:`, error instanceof Error ? error.message : 'Unknown error'); this.emit('drmPermissionsSetup', { instanceId, success: false, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }); } } /** * Verify DRM capabilities for the browser instance */ async verifyDrmCapabilities(page, instanceId) { try { const drmSupport = await page.evaluate(() => { return new Promise((resolve) => { // Check for Widevine support const checkWidevine = () => { // eslint-disable-next-line no-undef if (typeof globalThis.navigator?.requestMediaKeySystemAccess === 'function') { // eslint-disable-next-line no-undef globalThis.navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{ initDataTypes: ['cenc'], audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }], videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }] }]) .then(() => resolve({ widevine: true, error: null })) .catch((error) => resolve({ widevine: false, error: error?.message || 'Unknown error' })); } else { resolve({ widevine: false, error: 'MediaKeySystemAccess not available' }); } }; // Check for protected media identifier permission // eslint-disable-next-line no-undef if (typeof globalThis.navigator?.permissions !== 'undefined') { // eslint-disable-next-line no-undef globalThis.navigator.permissions.query({ name: 'protected-media-identifier' }) .then((result) => { if (result.state === 'granted') { checkWidevine(); } else { resolve({ widevine: false, error: `Protected media permission: ${result.state}` }); } }) .catch(() => checkWidevine()); // Fallback to direct check } else { checkWidevine(); } }); }); this.emit('drmCapabilitiesVerified', { instanceId, drmSupport, timestamp: new Date() }); if (!drmSupport.widevine) { console.warn(`DRM verification failed for instance ${instanceId}:`, drmSupport.error); } } catch (error) { this.emit('drmVerificationFailed', { instanceId, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }); } } /** * Clean up temporary DRM profile directory */ async cleanupDrmProfile(instanceId) { if (!this.config.drmConfig) { return; // No DRM profile to clean up } const profilePath = `/tmp/chrome-drm-profile-${instanceId}`; try { const fs = await Promise.resolve().then(() => __importStar(require('fs'))); // Check if profile directory exists if (fs.existsSync(profilePath)) { // Remove the entire profile directory recursively await fs.promises.rm(profilePath, { recursive: true, force: true }); this.emit('drmProfileCleaned', { instanceId, profilePath, timestamp: new Date() }); } } catch (error) { // Log warning but don't fail the cleanup console.warn(`Failed to clean up DRM profile for instance ${instanceId}:`, error instanceof Error ? error.message : 'Unknown error'); this.emit('drmProfileCleanupFailed', { instanceId, profilePath, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }); } } /** * Clean up all temporary DRM profiles (safety cleanup during shutdown) */ async cleanupAllDrmProfiles() { if (!this.config.drmConfig) { return; // No DRM profiles to clean up } try { const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); // Find all chrome-drm-profile directories in /tmp const tmpDir = '/tmp'; const files = await fs.promises.readdir(tmpDir); const drmProfiles = files.filter(file => file.startsWith('chrome-drm-profile-')); const cleanupPromises = drmProfiles.map(async (profileDir) => { const fullPath = path.join(tmpDir, profileDir); try { await fs.promises.rm(fullPath, { recursive: true, force: true }); console.log(`Cleaned up orphaned DRM profile: ${fullPath}`); } catch (error) { console.warn(`Failed to clean up orphaned DRM profile ${fullPath}:`, error instanceof Error ? error.message : 'Unknown error'); } }); await Promise.all(cleanupPromises); if (drmProfiles.length > 0) { this.emit('allDrmProfilesCleaned', { profileCount: drmProfiles.length, timestamp: new Date() }); } } catch (error) { console.warn('Failed to perform DRM profile cleanup:', error instanceof Error ? error.message : 'Unknown error'); } } /** * Initialize localStorage for all configured domains */ async initializeLocalStorage(page) { if (!this.config.localStorage || this.config.localStorage.length === 0) { return; } // Import randomization utility const { RandomizationUtil } = await Promise.resolve().then(() => __importStar(require('../utils/randomization'))); // Create randomization context with browser instance specific data const instanceId = `browser-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const randomizationUtil = new RandomizationUtil({ instanceId, timestamp: Date.now().toString(), sessionId: `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // Add any arrays that might be used in randomFrom functions userIds: ['user_001', 'user_002', 'user_003', 'user_004', 'user_005'], deviceTypes: ['desktop', 'mobile', 'tablet'], themes: ['light', 'dark', 'auto'], languages: ['en', 'es', 'fr', 'de', 'ja'], currencies: ['USD', 'EUR', 'GBP', 'JPY', 'CAD'], // Additional arrays for streaming/media applications videoQualities: ['480p', '720p', '1080p', '4K'], subscriptionTiers: ['free', 'basic', 'premium', 'enterprise'], booleans: ['true', 'false'], playbackSpeeds: ['0.5', '0.75', '1.0', '1.25', '1.5', '2.0'] }); for (const localStorageEntry of this.config.localStorage) { try { // Navigate to the domain to set localStorage const domainUrl = localStorageEntry.domain.startsWith('http') ? localStorageEntry.domain : `https://${localStorageEntry.domain}`; await page.goto(domainUrl, { waitUntil: 'domcontentloaded', timeout: 10000 }); // Process localStorage data with randomization const processedData = randomizationUtil.processLocalStorageData(localStorageEntry.data); // Set localStorage items for this domain await page.evaluate((data) => { Object.entries(data).forEach(([key, value]) => { globalThis.localStorage.setItem(key, value); }); }, processedData); this.emit('localStorageInitialized', { domain: localStorageEntry.domain, itemCount: Object.keys(processedData).length, processedData // Include processed data in event for debugging }); } catch (error) { this.emit('localStorageInitializationFailed', { domain: localStorageEntry.domain, error }); // Continue with other domains even if one fails } } // Navigate to about:blank after setting up localStorage await page.goto('about:blank'); } } exports.BrowserPool = BrowserPool; //# sourceMappingURL=browser-pool.js.map