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
JavaScript
"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