Initialize smartkvm package
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type {
|
||||
IBrowserKvmOptions,
|
||||
IKvmDriver,
|
||||
IKvmFrame,
|
||||
IKvmTypeTextOptions,
|
||||
TKvmKey,
|
||||
TKvmKind,
|
||||
} from './smartkvm.interfaces.js';
|
||||
|
||||
const defaultViewerSelector = 'video, canvas';
|
||||
|
||||
interface ICaptureMediaResult {
|
||||
captured: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dataBase64?: string;
|
||||
error?: string;
|
||||
hasMediaElement?: boolean;
|
||||
mediaHasFrame?: boolean;
|
||||
}
|
||||
|
||||
export class SmartBrowserKvm implements IKvmDriver {
|
||||
public readonly kind: TKvmKind;
|
||||
|
||||
private options: IBrowserKvmOptions;
|
||||
private browser?: plugins.puppeteer.Browser;
|
||||
private page?: plugins.puppeteer.Page;
|
||||
|
||||
constructor(options: IBrowserKvmOptions) {
|
||||
this.options = options;
|
||||
this.kind = options.kind ?? 'generic';
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (this.browser && this.page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutMs = this.options.timeoutMs ?? 30000;
|
||||
const args: string[] = [];
|
||||
if (process.env.CI || process.getuid?.() === 0) {
|
||||
args.push('--no-sandbox', '--disable-setuid-sandbox');
|
||||
}
|
||||
|
||||
this.browser = await plugins.puppeteer.launch({
|
||||
args,
|
||||
acceptInsecureCerts: this.options.ignoreHttpsErrors ?? false,
|
||||
defaultViewport: null,
|
||||
executablePath: this.options.executablePath,
|
||||
headless: this.options.headless ?? true,
|
||||
timeout: timeoutMs,
|
||||
userDataDir: this.options.userDataDir,
|
||||
});
|
||||
|
||||
this.page = await this.browser.newPage();
|
||||
this.page.setDefaultTimeout(timeoutMs);
|
||||
this.page.setDefaultNavigationTimeout(timeoutMs);
|
||||
|
||||
await this.page.goto(this.options.url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
|
||||
await this.tryGenericLogin();
|
||||
await this.waitForViewerReady();
|
||||
await this.focusViewer();
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
const browser = this.browser;
|
||||
this.page = undefined;
|
||||
this.browser = undefined;
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
public async focusViewer(): Promise<void> {
|
||||
const page = this.requirePage();
|
||||
const viewerSelector = this.getViewerSelector();
|
||||
const viewerElement = await page.$(viewerSelector);
|
||||
if (!viewerElement) {
|
||||
throw new Error(`KVM viewer selector missing: ${viewerSelector}`);
|
||||
}
|
||||
await viewerElement.click();
|
||||
}
|
||||
|
||||
public async captureFrame(): Promise<IKvmFrame> {
|
||||
const page = this.requirePage();
|
||||
const captureSelector = this.getCaptureSelector();
|
||||
const captureElement = await page.$(captureSelector);
|
||||
if (!captureElement) {
|
||||
throw new Error(`KVM capture selector missing: ${captureSelector}`);
|
||||
}
|
||||
|
||||
const mediaResult = await page.evaluate((selector: string): ICaptureMediaResult => {
|
||||
const rootElement = document.querySelector(selector);
|
||||
if (!rootElement) {
|
||||
return {
|
||||
captured: false,
|
||||
error: `KVM capture selector missing: ${selector}`,
|
||||
};
|
||||
}
|
||||
|
||||
const mediaElement = (rootElement.matches('video, canvas')
|
||||
? rootElement
|
||||
: rootElement.querySelector('video, canvas')) as HTMLVideoElement | HTMLCanvasElement | null;
|
||||
|
||||
if (!mediaElement) {
|
||||
return {
|
||||
captured: false,
|
||||
hasMediaElement: false,
|
||||
};
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
if (mediaElement instanceof HTMLVideoElement) {
|
||||
width = mediaElement.videoWidth;
|
||||
height = mediaElement.videoHeight;
|
||||
} else if (mediaElement instanceof HTMLCanvasElement) {
|
||||
width = mediaElement.width;
|
||||
height = mediaElement.height;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
return {
|
||||
captured: false,
|
||||
hasMediaElement: true,
|
||||
mediaHasFrame: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const canvasElement = document.createElement('canvas');
|
||||
canvasElement.width = width;
|
||||
canvasElement.height = height;
|
||||
const context = canvasElement.getContext('2d');
|
||||
if (!context) {
|
||||
return {
|
||||
captured: false,
|
||||
hasMediaElement: true,
|
||||
mediaHasFrame: true,
|
||||
error: 'Could not create canvas 2D context for KVM frame capture.',
|
||||
};
|
||||
}
|
||||
context.drawImage(mediaElement, 0, 0, width, height);
|
||||
const dataUrl = canvasElement.toDataURL('image/png');
|
||||
return {
|
||||
captured: true,
|
||||
width,
|
||||
height,
|
||||
dataBase64: dataUrl.slice(dataUrl.indexOf(',') + 1),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
captured: false,
|
||||
hasMediaElement: true,
|
||||
mediaHasFrame: true,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}, captureSelector);
|
||||
|
||||
if (mediaResult.captured) {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
width: mediaResult.width ?? 0,
|
||||
height: mediaResult.height ?? 0,
|
||||
mimeType: 'image/png',
|
||||
dataBase64: mediaResult.dataBase64 ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaResult.error?.startsWith('KVM capture selector missing')) {
|
||||
throw new Error(mediaResult.error);
|
||||
}
|
||||
|
||||
if (mediaResult.hasMediaElement && mediaResult.mediaHasFrame === false) {
|
||||
throw new Error(`KVM media element has no frame: ${captureSelector}`);
|
||||
}
|
||||
|
||||
const boundingBox = await captureElement.boundingBox();
|
||||
if (!boundingBox || boundingBox.width <= 0 || boundingBox.height <= 0) {
|
||||
throw new Error(`KVM capture selector has no visible frame: ${captureSelector}`);
|
||||
}
|
||||
|
||||
const screenshot = await captureElement.screenshot({
|
||||
type: 'png',
|
||||
});
|
||||
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
width: Math.round(boundingBox.width),
|
||||
height: Math.round(boundingBox.height),
|
||||
mimeType: 'image/png',
|
||||
dataBase64: Buffer.from(screenshot).toString('base64'),
|
||||
};
|
||||
}
|
||||
|
||||
public async typeText(text: string, options?: IKvmTypeTextOptions): Promise<void> {
|
||||
const page = this.requirePage();
|
||||
await this.focusViewer();
|
||||
await page.keyboard.type(text, {
|
||||
delay: options?.delayMs,
|
||||
});
|
||||
}
|
||||
|
||||
public async pressKey(key: TKvmKey): Promise<void> {
|
||||
const page = this.requirePage();
|
||||
await this.focusViewer();
|
||||
await page.keyboard.press(key as plugins.puppeteer.KeyInput);
|
||||
}
|
||||
|
||||
public async pressShortcut(keys: TKvmKey[]): Promise<void> {
|
||||
const page = this.requirePage();
|
||||
await this.focusViewer();
|
||||
|
||||
const pressedKeys: TKvmKey[] = [];
|
||||
try {
|
||||
for (const key of keys) {
|
||||
await page.keyboard.down(key as plugins.puppeteer.KeyInput);
|
||||
pressedKeys.push(key);
|
||||
}
|
||||
} finally {
|
||||
for (const key of pressedKeys.reverse()) {
|
||||
await page.keyboard.up(key as plugins.puppeteer.KeyInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async wait(milliseconds: number): Promise<void> {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
private async tryGenericLogin(): Promise<void> {
|
||||
if (!this.options.username || !this.options.password) {
|
||||
return;
|
||||
}
|
||||
|
||||
const page = this.requirePage();
|
||||
const usernameElement = await this.findFirstElement([
|
||||
'input[name="username"]',
|
||||
'input[autocomplete="username"]',
|
||||
'input[type="email"]',
|
||||
'input[type="text"]',
|
||||
]);
|
||||
const passwordElement = await this.findFirstElement([
|
||||
'input[name="password"]',
|
||||
'input[autocomplete="current-password"]',
|
||||
'input[type="password"]',
|
||||
]);
|
||||
|
||||
if (!usernameElement || !passwordElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
await usernameElement.click({ clickCount: 3 });
|
||||
await usernameElement.type(this.options.username);
|
||||
await passwordElement.click({ clickCount: 3 });
|
||||
await passwordElement.type(this.options.password);
|
||||
await passwordElement.press('Enter');
|
||||
|
||||
await Promise.race([
|
||||
page
|
||||
.waitForNavigation({
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: Math.min(this.options.timeoutMs ?? 30000, 10000),
|
||||
})
|
||||
.catch(() => undefined),
|
||||
this.wait(1000),
|
||||
]);
|
||||
}
|
||||
|
||||
private async waitForViewerReady(): Promise<void> {
|
||||
const page = this.requirePage();
|
||||
const viewerSelector = this.getViewerSelector();
|
||||
await page.waitForFunction(
|
||||
(selector: string) => {
|
||||
const rootElement = document.querySelector(selector);
|
||||
if (!rootElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mediaElement = (rootElement.matches('video, canvas')
|
||||
? rootElement
|
||||
: rootElement.querySelector('video, canvas')) as HTMLVideoElement | HTMLCanvasElement | null;
|
||||
|
||||
if (mediaElement instanceof HTMLVideoElement) {
|
||||
return mediaElement.videoWidth > 0 && mediaElement.videoHeight > 0;
|
||||
}
|
||||
|
||||
if (mediaElement instanceof HTMLCanvasElement) {
|
||||
return mediaElement.width > 0 && mediaElement.height > 0;
|
||||
}
|
||||
|
||||
const boundingRect = rootElement.getBoundingClientRect();
|
||||
return boundingRect.width > 0 && boundingRect.height > 0;
|
||||
},
|
||||
{
|
||||
timeout: this.options.timeoutMs ?? 30000,
|
||||
},
|
||||
viewerSelector
|
||||
);
|
||||
}
|
||||
|
||||
private async findFirstElement(
|
||||
selectors: string[]
|
||||
): Promise<plugins.puppeteer.ElementHandle<Element> | null> {
|
||||
const page = this.requirePage();
|
||||
for (const selector of selectors) {
|
||||
const element = await page.$(selector);
|
||||
if (element) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getViewerSelector(): string {
|
||||
return this.options.viewerSelector ?? defaultViewerSelector;
|
||||
}
|
||||
|
||||
private getCaptureSelector(): string {
|
||||
return this.options.captureSelector ?? this.options.viewerSelector ?? defaultViewerSelector;
|
||||
}
|
||||
|
||||
private requirePage(): plugins.puppeteer.Page {
|
||||
if (!this.page) {
|
||||
throw new Error('SmartBrowserKvm is not connected. Call connect() first.');
|
||||
}
|
||||
return this.page;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user