import { IImageProcessorLogger, ImageProcessor } from '@lenovo-software/lsa-clients-dom-renderer'; import { ImageFormatsSupported } from '@lenovo-software/lsa-clients-dom-renderer/dist/image/imageProcessor'; import { Logger } from '../common/logger'; export enum ScreenSource { ActiveTab = 0, FullScreen = 1, Unknown = 2 } export interface ScreenCaptureRequest { width?: number; height?: number; source: ScreenSource; } export interface ScreenCaptureResponse { width?: number; height?: number; imageBase64: string; } export abstract class ScreenCapture { // Maintain the reference to last used screen source by user. protected static lastScreenSource: ScreenSource = ScreenSource.ActiveTab; protected logger = Logger.getInstance(); private listenerRegistered = false; private genericImage = new Image(); private genericImageCacheMap: Map = new Map(); private maxExtensionResponseWaitTime: number = 2000; private extensionTimeoutId: any; // Maintains list of all resolves that are pending private resolves: any[] = new Array(); // Maintains list of all rejects that are pending. private rejects: any[] = new Array(); constructor(externalExtensionEvent: string, genericImagePath: string) { this.genericImage.src = genericImagePath; if (!this.listenerRegistered) { this.registerExternalExtensionEventListener(externalExtensionEvent); } } /** * * Provide logic on how to get the full screen image * * @param request {ScreenCaptureRequest} */ protected abstract captureFullScreen(request: ScreenCaptureRequest): Promise; /** * * Provide logic on how to get the Active tab screen image * * @param request {ScreenCaptureRequest} */ protected abstract captureActiveTab(request: ScreenCaptureRequest): Promise; /** * Get the screen image * * @param request {ScreenCaptureRequest} Screen Request object * * @returns Promise */ protected getScreenCapture(request: ScreenCaptureRequest): Promise { this.logger.logMessage(`Screen capture request received for -> source: ${request.source}`); return new Promise(async (resolve, reject) => { if (request.source === ScreenSource.FullScreen) { this.logger.logMessage(`Processing Full screen capture request`); ScreenCapture.lastScreenSource = ScreenSource.FullScreen; try { this.logger.logMessage(`Full screen capture received`); resolve(await this.captureFullScreen(request)); } catch (err) { this.logger.logError( `Unable to load full screen capture, either the system is waiting for permission or capture generation failed.` ); resolve(await this.getGenericImage(request, 'jpeg')); } } else { this.logger.logMessage(`Processing Active tab capture request`); // This code stops the full screen capture as user switched source from Full Screen to active tab if (ScreenCapture.lastScreenSource === ScreenSource.FullScreen) { ScreenCapture.lastScreenSource = ScreenSource.ActiveTab; // Stop Full screen capture chrome.runtime.sendMessage({ message: 'StopThumbnailCapture' }); } try { // Push the promis resolves to array, the functions will be evaluated in the listener defined in constructor this.resolves.push(resolve); // Push the promis rejects to array, the functions will be evaluated in the listener defined in constructor this.rejects.push(reject); // Sends messages to extensions for getting active tab screen capture. // The response of this function is received in the listener defined in constructor. await this.captureActiveTab(request); // Check if we received any respone from any extension in a specified time. // This timeout section has a race condition where we may only set this timer about every other // iteration through the refresh period // Check if last call was not completed, as it is still waiting for either resolve/reject to be called. if (this.extensionTimeoutId == null) { this.extensionTimeoutId = setTimeout(async () => { this.logger.logError( 'Active tab screen capture retrieval failed. Extension did not respond in ' + this.maxExtensionResponseWaitTime + 'ms.' ); // Set generic image into response. this.resolveAllRequests(await this.getGenericImage(request, 'jpeg')); }, this.maxExtensionResponseWaitTime); } } catch (err) { this.logger.logError(`Active tab screen capture request failed`); this.rejectAllRequests(); } } }); } /** * Register listener to external extension messages * * @param eventName {string} Event to listen for response * */ private registerExternalExtensionEventListener(eventName: string): void { // Add a listener to messages from external plugins. chrome.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { if (request.message) { switch (request.message) { case eventName: { this.logger.logMessage(`Event received from extension for event: ${eventName}`); const response = ScreenCaptureResponseFactory.create( request.base64, request.width, request.height ); // Clear the time for extension Response timeout clearTimeout(this.extensionTimeoutId); this.extensionTimeoutId = null; this.resolveAllRequests(response); } default: { break; } } } if (sendResponse) { // messsage requested a response? sendResponse({ status: 'OK' }); } }); } /** * Resolves all pending image requests and clear up the pending reject requests * * @param response {ScreenCaptureResponse} Response to be sent to user * */ private resolveAllRequests(response: ScreenCaptureResponse): void { // Resolves all the pending promises this.resolves.forEach((resolve: any) => { resolve(response); }); // Reset the pending requests array. this.resolves = []; this.rejects = []; } /** * Rejects all image requests and clear up the pending resolve requests */ private rejectAllRequests(): void { // Rejects all the pending promises this.rejects.forEach((reject: any) => { reject(); }); // Reset the pending requests array. this.resolves = []; this.rejects = []; } /** * Generates the generic image for size as specified in imageParams * The function uses a map to cache the generated image and reuse them * when requested. * * @param request {ScreenCaptureRequest} Image request object * @param format {ImageFormatsSupported} Format of image in output * * @returns Promise * */ private async getGenericImage( request: ScreenCaptureRequest, format: ImageFormatsSupported ): Promise { try { let genericImageBuffer: any = ''; // The key used to cache the generated generic image in hash map const cacheKey = `${request.width}x${request.height}-${format}`; if (this.genericImageCacheMap.has(cacheKey)) { // Get the image buffer from cache hash map genericImageBuffer = this.genericImageCacheMap.get(cacheKey); } else { // Generate new image and add it to cache hash map let imgBMP = await createImageBitmap(this.genericImage); genericImageBuffer = await new ImageProcessor(new genericImageLogger(this.logger)).processImage( imgBMP, { width: request.width, height: request.height, format } ); this.genericImageCacheMap.set(cacheKey, genericImageBuffer); } return ScreenCaptureResponseFactory.create(genericImageBuffer, request.width, request.height); } catch (err) { this.logger.logError(`Error creating generic image`); return ScreenCaptureResponseFactory.create('', 0, 0); } } } export class ScreenCaptureResponseFactory { public static create(imageBase64: string, width?: number, height?: number): ScreenCaptureResponse { return { width, height, imageBase64 }; } } class genericImageLogger implements IImageProcessorLogger { private logger: Logger; constructor(_logger: Logger) { this.logger = _logger; } logDebug(msg: string) { this.logger.logDebug(msg); } logInfo(msg: string) { this.logger.logInfo(msg); } logMessage(msg: string) { this.logger.logMessage(msg); } logWarning(msg: string) { this.logger.logWarning(msg); } logError(msg: string) { this.logger.logError(msg); } }