| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| |
| import {OverlayModel} from './OverlayModel.js'; |
| import {SDKModel} from './SDKModel.js'; |
| import {Capability, type Target} from './Target.js'; |
| |
| export const enum ScreenshotMode { |
| FROM_VIEWPORT = 'fromViewport', |
| FROM_CLIP = 'fromClip', |
| FULLPAGE = 'fullpage', |
| } |
| |
| /** |
| * This structure holds a specific `startScreencast` request's parameters |
| * and its callbacks so that they can be re-started if needed. |
| **/ |
| interface ScreencastOperation { |
| id: number; |
| request: { |
| format: Protocol.Page.StartScreencastRequestFormat, |
| quality: number, |
| maxWidth: number|undefined, |
| maxHeight: number|undefined, |
| everyNthFrame: number|undefined, |
| }; |
| callbacks: { |
| onScreencastFrame: ScreencastFrameCallback, |
| onScreencastVisibilityChanged: ScreencastVisibilityChangedCallback, |
| }; |
| } |
| |
| type ScreencastFrameCallback = ((arg0: Protocol.binary, arg1: Protocol.Page.ScreencastFrameMetadata) => void); |
| type ScreencastVisibilityChangedCallback = ((arg0: boolean) => void); |
| |
| /** |
| * Manages concurrent screencast requests by queuing and prioritizing. |
| * |
| * When startScreencast is invoked: |
| * - If a screencast is currently active, the existing screencast's parameters and callbacks are |
| * saved in the #screencastOperations array. |
| * - The active screencast is then stopped. |
| * - A new screencast is initiated using the parameters and callbacks from the current startScreencast call. |
| * |
| * When stopScreencast is invoked: |
| * - The currently active screencast is stopped. |
| * - The #screencastOperations is checked for interrupted screencast operations. |
| * - If any operations are found, the latest one is started |
| * using its saved parameters and callbacks. |
| * |
| * This ensures that: |
| * - Only one screencast is active at a time. |
| * - Interrupted screencasts are resumed after the current screencast is stopped. |
| * This ensures animation previews, which use screencasting, don't disrupt ongoing remote debugging sessions. Without this mechanism, stopping a preview screencast would terminate the debugging screencast, freezing the ScreencastView. |
| **/ |
| export class ScreenCaptureModel extends SDKModel<void> implements ProtocolProxyApi.PageDispatcher { |
| readonly #agent: ProtocolProxyApi.PageApi; |
| #nextScreencastOperationId = 1; |
| #screencastOperations: ScreencastOperation[] = []; |
| constructor(target: Target) { |
| super(target); |
| this.#agent = target.pageAgent(); |
| target.registerPageDispatcher(this); |
| } |
| |
| async startScreencast( |
| format: Protocol.Page.StartScreencastRequestFormat, quality: number, maxWidth: number|undefined, |
| maxHeight: number|undefined, everyNthFrame: number|undefined, onFrame: ScreencastFrameCallback, |
| onVisibilityChanged: ScreencastVisibilityChangedCallback): Promise<number> { |
| const currentRequest = this.#screencastOperations.at(-1); |
| if (currentRequest) { |
| // If there already is a screencast operation in progress, we need to stop it now and handle the |
| // incoming request. Once that request is stopped, we'll return back to handling the stopped operation. |
| await this.#agent.invoke_stopScreencast(); |
| } |
| |
| const operation = { |
| id: this.#nextScreencastOperationId++, |
| request: { |
| format, |
| quality, |
| maxWidth, |
| maxHeight, |
| everyNthFrame, |
| }, |
| callbacks: { |
| onScreencastFrame: onFrame, |
| onScreencastVisibilityChanged: onVisibilityChanged, |
| } |
| }; |
| this.#screencastOperations.push(operation); |
| void this.#agent.invoke_startScreencast({format, quality, maxWidth, maxHeight, everyNthFrame}); |
| |
| return operation.id; |
| } |
| |
| stopScreencast(id: number): void { |
| const operationToStop = this.#screencastOperations.pop(); |
| if (!operationToStop) { |
| throw new Error('There is no screencast operation to stop.'); |
| } |
| |
| if (operationToStop.id !== id) { |
| throw new Error('Trying to stop a screencast operation that is not being served right now.'); |
| } |
| void this.#agent.invoke_stopScreencast(); |
| |
| // The latest operation is concluded, let's return back to the previous request now, if it exists. |
| const nextOperation = this.#screencastOperations.at(-1); |
| if (nextOperation) { |
| void this.#agent.invoke_startScreencast({ |
| format: nextOperation.request.format, |
| quality: nextOperation.request.quality, |
| maxWidth: nextOperation.request.maxWidth, |
| maxHeight: nextOperation.request.maxHeight, |
| everyNthFrame: nextOperation.request.everyNthFrame, |
| }); |
| } |
| } |
| |
| async captureScreenshot( |
| format: Protocol.Page.CaptureScreenshotRequestFormat, quality: number, mode: ScreenshotMode, |
| clip?: Protocol.Page.Viewport): Promise<string|null> { |
| const properties: Protocol.Page.CaptureScreenshotRequest = { |
| format, |
| quality, |
| fromSurface: true, |
| }; |
| switch (mode) { |
| case ScreenshotMode.FROM_CLIP: |
| properties.captureBeyondViewport = true; |
| properties.clip = clip; |
| break; |
| case ScreenshotMode.FULLPAGE: |
| properties.captureBeyondViewport = true; |
| break; |
| case ScreenshotMode.FROM_VIEWPORT: |
| properties.captureBeyondViewport = false; |
| break; |
| default: |
| throw new Error('Unexpected or unspecified screnshotMode'); |
| } |
| |
| await OverlayModel.muteHighlight(); |
| const result = await this.#agent.invoke_captureScreenshot(properties); |
| await OverlayModel.unmuteHighlight(); |
| return result.data; |
| } |
| |
| screencastFrame({data, metadata, sessionId}: Protocol.Page.ScreencastFrameEvent): void { |
| void this.#agent.invoke_screencastFrameAck({sessionId}); |
| |
| const currentRequest = this.#screencastOperations.at(-1); |
| if (currentRequest) { |
| currentRequest.callbacks.onScreencastFrame.call(null, data, metadata); |
| } |
| } |
| |
| screencastVisibilityChanged({visible}: Protocol.Page.ScreencastVisibilityChangedEvent): void { |
| const currentRequest = this.#screencastOperations.at(-1); |
| if (currentRequest) { |
| currentRequest.callbacks.onScreencastVisibilityChanged.call(null, visible); |
| } |
| } |
| |
| backForwardCacheNotUsed(_params: Protocol.Page.BackForwardCacheNotUsedEvent): void { |
| } |
| |
| domContentEventFired(_params: Protocol.Page.DomContentEventFiredEvent): void { |
| } |
| |
| loadEventFired(_params: Protocol.Page.LoadEventFiredEvent): void { |
| } |
| |
| lifecycleEvent(_params: Protocol.Page.LifecycleEventEvent): void { |
| } |
| |
| navigatedWithinDocument(_params: Protocol.Page.NavigatedWithinDocumentEvent): void { |
| } |
| |
| frameAttached(_params: Protocol.Page.FrameAttachedEvent): void { |
| } |
| |
| frameNavigated(_params: Protocol.Page.FrameNavigatedEvent): void { |
| } |
| |
| documentOpened(_params: Protocol.Page.DocumentOpenedEvent): void { |
| } |
| |
| frameDetached(_params: Protocol.Page.FrameDetachedEvent): void { |
| } |
| |
| frameStartedLoading(_params: Protocol.Page.FrameStartedLoadingEvent): void { |
| } |
| |
| frameStoppedLoading(_params: Protocol.Page.FrameStoppedLoadingEvent): void { |
| } |
| |
| frameRequestedNavigation(_params: Protocol.Page.FrameRequestedNavigationEvent): void { |
| } |
| |
| frameStartedNavigating(_params: Protocol.Page.FrameStartedNavigatingEvent): void { |
| } |
| |
| frameSubtreeWillBeDetached(_params: Protocol.Page.FrameSubtreeWillBeDetachedEvent): void { |
| } |
| |
| frameScheduledNavigation(_params: Protocol.Page.FrameScheduledNavigationEvent): void { |
| } |
| |
| frameClearedScheduledNavigation(_params: Protocol.Page.FrameClearedScheduledNavigationEvent): void { |
| } |
| |
| frameResized(): void { |
| } |
| |
| javascriptDialogOpening(_params: Protocol.Page.JavascriptDialogOpeningEvent): void { |
| } |
| |
| javascriptDialogClosed(_params: Protocol.Page.JavascriptDialogClosedEvent): void { |
| } |
| |
| interstitialShown(): void { |
| } |
| |
| interstitialHidden(): void { |
| } |
| |
| windowOpen(_params: Protocol.Page.WindowOpenEvent): void { |
| } |
| |
| fileChooserOpened(_params: Protocol.Page.FileChooserOpenedEvent): void { |
| } |
| |
| compilationCacheProduced(_params: Protocol.Page.CompilationCacheProducedEvent): void { |
| } |
| |
| downloadWillBegin(_params: Protocol.Page.DownloadWillBeginEvent): void { |
| } |
| |
| downloadProgress(): void { |
| } |
| |
| prefetchStatusUpdated(_params: Protocol.Preload.PrefetchStatusUpdatedEvent): void { |
| } |
| |
| prerenderStatusUpdated(_params: Protocol.Preload.PrerenderStatusUpdatedEvent): void { |
| } |
| } |
| |
| SDKModel.register(ScreenCaptureModel, {capabilities: Capability.SCREEN_CAPTURE, autostart: false}); |