// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from '//resources/js/assert.js';
import type {BigBuffer} from '//resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
import type {BitmapMappedFromTrustedProcess} from '//resources/mojo/skia/public/mojom/bitmap.mojom-webui.js';
import {BrowserProxyImpl} from './browser_proxy.js';
/**
* @fileoverview A browser proxy for receiving the viewport screenshot from the
* browser.
*/
let instance: ScreenshotBitmapBrowserProxy|null = null;
type ScreenshotReceivedCallback = (screenshotBitmap: ImageBitmap) => void;
export interface ScreenshotBitmapBrowserProxy {
// Returns the screenshot from the browser process. If the screenshot has been
// sent already, the promise will return immediately. Else, the promise will
// resolve once the screenshot has been retrieved.
fetchScreenshot(callback: ScreenshotReceivedCallback): void;
}
export class ScreenshotBitmapBrowserProxyImpl implements
ScreenshotBitmapBrowserProxy {
private screenshot?: ImageBitmap;
private screenshotListenerId: number;
private callbacks: ScreenshotReceivedCallback[] = [];
constructor() {
this.screenshotListenerId =
BrowserProxyImpl.getInstance()
.callbackRouter.screenshotDataReceived.addListener(
this.screenshotDataReceived.bind(this));
}
static getInstance(): ScreenshotBitmapBrowserProxy {
return instance || (instance = new ScreenshotBitmapBrowserProxyImpl());
}
static setInstance(obj: ScreenshotBitmapBrowserProxy) {
instance = obj;
}
fetchScreenshot(callback: ScreenshotReceivedCallback): void {
if (this.screenshot) {
// We need to make a new bitmap because each canvas takes ownership of the
// bitmap, so it cannot be drawn to multiple HTMLCanvasElement.
createImageBitmap(this.screenshot).then((bitmap) => {
callback(bitmap);
});
return;
}
// Queue the callback for when the screenshot is ready.
this.callbacks.push(callback);
}
private async screenshotDataReceived(screenshotData:
BitmapMappedFromTrustedProcess) {
const data: BigBuffer = screenshotData.pixelData;
// TODO(b/334185985): This occurs when the browser failed to allocate the
// memory for the pixels. Handle this case.
if (data.invalidBuffer) {
return;
}
// Pull the pixel data into a Uint8ClampedArray.
let pixelData: Uint8ClampedArray;
if (Array.isArray(data.bytes)) {
pixelData = new Uint8ClampedArray(data.bytes);
} else {
// If the buffer is not invalid or an array, it must be shared memory.
assert(data.sharedMemory);
const sharedMemory = data.sharedMemory;
const {buffer, result} =
sharedMemory.bufferHandle.mapBuffer(0, sharedMemory.size);
assert(result === Mojo.RESULT_OK);
pixelData = new Uint8ClampedArray(buffer);
}
const imageWidth = screenshotData.imageInfo.width;
const imageHeight = screenshotData.imageInfo.height;
// Put our screenshot into ImageData object so it can be rendered in a
// Canvas.
const imageData = new ImageData(pixelData, imageWidth, imageHeight);
const imageBitmap = await createImageBitmap(imageData);
// Cache the bitmap for future requests
this.screenshot = imageBitmap;
// Send the screenshot to all the callbacks.
for (const callback of this.callbacks) {
// We need to make a new bitmap because each canvas takes ownership of the
// bitmap, so it cannot be drawn to multiple HTMLCanvasElement.
createImageBitmap(this.screenshot).then((bitmap) => {
callback(bitmap);
});
}
this.callbacks = [];
// Stop listening for new screenshots.
assert(BrowserProxyImpl.getInstance().callbackRouter.removeListener(
this.screenshotListenerId));
}
}