import { AbstractInterface } from '@midscene/core/device';
import type { ActionParam } from '@midscene/core';
import type { ActionReturn } from '@midscene/core';
import type { ADB } from 'appium-adb';
import type { Adb } from '@yume-chan/adb';
import { Agent } from '@midscene/core/agent';
import { AgentBehaviorInitArgs } from '@midscene/shared/agent-tools/agent-behavior-init-args';
import { AgentOpt } from '@midscene/core/agent';
import { AndroidDeviceInputOpt } from '@midscene/core/device';
import { AndroidDeviceOpt } from '@midscene/core/device';
import { BaseMidsceneTools } from '@midscene/shared/agent-tools/base-tools';
import type { Device } from 'appium-adb';
import { DeviceAction } from '@midscene/core';
import type { ElementInfo } from '@midscene/shared/extractor';
import { InitArgSpec } from '@midscene/shared/agent-tools/base-tools';
import { InterfaceType } from '@midscene/core';
import { MobileInputPrimitives } from '@midscene/core/device';
import { overrideAIConfig } from '@midscene/shared/env';
import { Point } from '@midscene/core';
import { Size } from '@midscene/core';
import type { ToolDefinition } from '@midscene/shared/agent-tools/types';
import { UITreeSnapshot } from '@midscene/core';

declare type ActionArgs<T extends DeviceAction> = [ActionParam<T>] extends [undefined] ? [] : [ActionParam<T>];

export declare function agentFromAdbDevice(deviceId?: string, opts?: AndroidAgentOpt & AndroidDeviceOpt): Promise<AndroidAgent>;

export declare class AndroidAgent extends Agent<AndroidDevice> {
    /**
     * Trigger the system back operation on Android devices
     */
    back: WrappedAction<DeviceActionAndroidBackButton>;
    /**
     * Trigger the system home operation on Android devices
     */
    home: WrappedAction<DeviceActionAndroidHomeButton>;
    /**
     * Trigger the system recent apps operation on Android devices
     */
    recentApps: WrappedAction<DeviceActionAndroidRecentAppsButton>;
    /**
     * User-provided app name to package name mapping
     */
    private appNameMapping;
    constructor(device: AndroidDevice, opts?: AndroidAgentOpt);
    /**
     * Launch an Android app or URL
     * @param uri - App package name, URL, or app name to launch
     */
    launch(uri: string): Promise<void>;
    /**
     * Terminate (force-stop) an Android app by package name
     * @param uri - Package name or app name to terminate
     */
    terminate(uri: string): Promise<void>;
    /**
     * Execute ADB shell command on Android device
     * @param command - ADB shell command to execute
     * @param opt - Optional ADB shell execution settings
     */
    runAdbShell(command: string, opt?: RunAdbShellOpt): Promise<string>;
    private createActionWrapper;
}

export declare type AndroidAgentOpt = AgentOpt & {
    /**
     * Custom mapping of app names to package names
     * User-provided mappings will take precedence over default mappings
     */
    appNameMapping?: Record<string, string>;
};

export declare interface AndroidConnectedDevice extends Device {
    model?: string;
    brand?: string;
    resolution?: string;
    density?: number;
}

export declare class AndroidDevice implements AbstractInterface {
    private deviceId;
    private yadbPushed;
    private devicePixelRatio;
    private devicePixelRatioInitialized;
    private adb;
    private connectingAdb;
    private destroyed;
    private description;
    private customActions?;
    private cachedScreenSize;
    private cachedOrientation;
    private cachedPhysicalDisplayId;
    private scrcpyAdapter;
    /**
     * Continuous frame-source capability for UI observation, sourced from the
     * scrcpy video stream. Only wired up when scrcpy is opt-in enabled (mirrors
     * iOS WDA MJPEG); otherwise left undefined so observers fall back to
     * sequential screenshots.
     */
    openFrameSource?: AbstractInterface['openFrameSource'];
    private appNameMapping;
    private cachedAdjustScale;
    private takeScreenshotFailCount;
    private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
    private static readonly DEFAULT_MIN_SCREENSHOT_BUFFER_SIZE;
    interfaceType: InterfaceType;
    uri: string | undefined;
    options?: AndroidDeviceOpt;
    private readonly visualActions;
    readonly inputPrimitives: MobileInputPrimitives;
    actionSpace(): DeviceAction<any>[];
    private performPullGesture;
    private performActionScroll;
    private runAdbShellRaw;
    constructor(deviceId: string, options?: AndroidDeviceOpt);
    describe(): string;
    connect(): Promise<ADB>;
    getAdb(): Promise<ADB>;
    private createAdbProxy;
    /** Current scrcpy configuration, connection, and recovery state. */
    getScrcpyStatus(): ScrcpyStatus;
    /** Retry scrcpy initialization without recreating the AndroidDevice. */
    retryScrcpy(): Promise<ScrcpyStatus>;
    /**
     * Get or create the scrcpy adapter (lazy initialization)
     */
    private getScrcpyAdapter;
    /**
     * Continuous frame source backed by the scrcpy video stream.
     *
     * Decoding H.264 to JPEG costs an ffmpeg process per frame (~100ms measured
     * on-device), so `latest()` hands out RAW keyframe handles (near-zero cost —
     * the stream is already flowing) and `decode()` pays the ffmpeg cost only
     * for the frames the observer actually sampled, at the end of the window.
     * Subscribing also keeps the scrcpy connection alive (each incoming frame
     * resets the idle timer) for the whole observation window.
     *
     * Opt-in: only wired up as the `openFrameSource` capability (in the
     * constructor) when `scrcpyConfig.enabled` is set. Throws if the stream is
     * unavailable; observers fall back to sequential `screenshotBase64()`.
     */
    private openScrcpyFrameSource;
    /**
     * Get device physical info needed by scrcpy adapter
     */
    private getDevicePhysicalInfo;
    /**
     * Set the app name to package name mapping
     */
    setAppNameMapping(mapping: Record<string, string>): void;
    /**
     * Resolve app name to package name using the mapping
     * Comparison is case-insensitive and ignores spaces, dashes, and underscores.
     * Keys in appNameMapping are pre-normalized, so we only need to normalize the input.
     * @param appName The app name to resolve
     */
    private resolvePackageName;
    launch(uri: string): Promise<AndroidDevice>;
    private launchRaw;
    /**
     * Terminate (force-stop) an Android app by package name.
     * Supports app name resolution via setAppNameMapping.
     * If uri contains "/" (e.g. com.example.app/.MainActivity), only the package part is used.
     */
    terminate(uri: string): Promise<void>;
    private terminateRaw;
    execYadb(keyboardContent: string): Promise<void>;
    private execYadbRaw;
    getElementsInfo(): Promise<ElementInfo[]>;
    getElementsNodeTree(): Promise<any>;
    getUITree(): Promise<UITreeSnapshot>;
    getScreenSize(): Promise<{
        override: string;
        physical: string;
        orientation: number;
        isCurrentOrientation?: boolean;
    }>;
    private initializeDevicePixelRatio;
    getDisplayDensity(): Promise<number>;
    getDisplayOrientation(): Promise<number>;
    /**
     * Get physical screen dimensions adjusted for current orientation.
     * Swaps width/height when the device is in landscape and the reported
     * dimensions do not already reflect the current orientation.
     */
    private getOrientedPhysicalSize;
    size(): Promise<Size>;
    /**
     * Compute and cache the coordinate adjustment scale by comparing
     * physical dimensions with logical dimensions from size().
     * Cached after first call; invalidated on destroy().
     */
    private getAdjustScale;
    /**
     * Convert logical coordinates (from AI) back to physical coordinates (for ADB).
     * The ratio is derived from size(), so overriding size() alone is sufficient.
     */
    private adjustCoordinates;
    /**
     * Calculate the end point for scroll operations based on start point, scroll delta, and screen boundaries.
     * This method ensures that scroll operations stay within screen bounds and maintain a minimum scroll distance
     * for effective scrolling gestures on Android devices.
     *
     * @param start - The starting point of the scroll gesture
     * @param deltaX - The horizontal scroll distance (positive = scroll right, negative = scroll left)
     * @param deltaY - The vertical scroll distance (positive = scroll down, negative = scroll up)
     * @param maxWidth - The maximum width boundary (screen width)
     * @param maxHeight - The maximum height boundary (screen height)
     * @returns The calculated end point for the scroll gesture
     */
    private calculateScrollEndPoint;
    private warnScrollDistanceClamped;
    screenshotBase64(): Promise<string>;
    /**
     * Keep independently captured fallback images within the same maximum
     * dimension as scrcpy frames. Disabled scrcpy and maxSize=0 preserve the
     * original ADB/yadb image without parsing it.
     */
    private prepareFallbackScreenshot;
    private captureScreenshotBase64FromDeviceFile;
    /**
     * Capture a screenshot directly via the yadb tool, bypassing scrcpy,
     * adb.takeScreenshot, and screencap. Used when the screenshotStrategy is
     * 'always-yadb', e.g. when screencap produces black frames for secure
     * (FLAG_SECURE) content but yadb captures it correctly.
     */
    private screenshotBase64ViaYadb;
    clearInput(element?: ElementInfo): Promise<void>;
    private clearInputRaw;
    private clearInputWithKeyboard;
    forceScreenshot(path: string): Promise<void>;
    url(): Promise<string>;
    scrollUntilTop(startPoint?: Point): Promise<void>;
    private scrollUntilTopRaw;
    scrollUntilBottom(startPoint?: Point): Promise<void>;
    private scrollUntilBottomRaw;
    scrollUntilLeft(startPoint?: Point): Promise<void>;
    private scrollUntilLeftRaw;
    scrollUntilRight(startPoint?: Point): Promise<void>;
    private scrollUntilRightRaw;
    scrollUp(distance?: number, startPoint?: Point): Promise<void>;
    private scrollUpRaw;
    scrollDown(distance?: number, startPoint?: Point): Promise<void>;
    private scrollDownRaw;
    scrollLeft(distance?: number, startPoint?: Point): Promise<void>;
    private scrollLeftRaw;
    scrollRight(distance?: number, startPoint?: Point): Promise<void>;
    private scrollRightRaw;
    ensureYadb(): Promise<void>;
    private resolveYadbBinPath;
    /**
     * Check if text contains characters that may cause issues with ADB inputText.
     * appium-adb's inputText has known bugs with certain characters:
     * - Backslash causes broken shell quoting
     * - Backtick is not escaped at all
     * - Text containing both " and ' throws an error
     * - Dollar sign can cause variable expansion issues
     *
     * For these characters, we route through yadb which handles them correctly
     * via escapeForShell + double-quoted shell context.
     */
    private shouldUseYadbForText;
    private typeText;
    private normalizeKeyName;
    private pressKey;
    private tapPoint;
    private doubleTapPoint;
    mouseMove(): Promise<void>;
    private dragPoint;
    private swipePoint;
    scroll(deltaX: number, deltaY: number, duration?: number, warnOnClamp?: boolean, direction?: ScrollDirection): Promise<void>;
    private scrollRaw;
    destroy(): Promise<void>;
    /**
     * Get the current device-local time as a formatted string.
     * This avoids formatting an Android epoch timestamp in the host machine's
     * timezone, which can disagree with the device status bar.
     */
    getDeviceLocalTimeString(format?: string): Promise<string>;
    back(): Promise<void>;
    private backRaw;
    home(): Promise<void>;
    private homeRaw;
    recentApps(): Promise<void>;
    private recentAppsRaw;
    private longPressPoint;
    pullDown(startPoint?: Point, distance?: number, duration?: number): Promise<void>;
    private pullDownRaw;
    pullDrag(from: {
        x: number;
        y: number;
    }, to: {
        x: number;
        y: number;
    }, duration: number): Promise<void>;
    private pullDragRaw;
    pullUp(startPoint?: Point, distance?: number, duration?: number): Promise<void>;
    private pullUpRaw;
    private getDisplayArg;
    /**
     * Send one or more Android keyevent codes via `input -d <id> keyevent`.
     * Centralizes the display-aware keyevent primitive so callers don't have
     * to hand-write `input${displayArg} keyevent` everywhere.
     */
    private shellInputKeyevent;
    /**
     * Send text via `input -d <id> text` with proper shell escaping and
     * optional per-character typing delay.
     *
     * Handles:
     * - Display targeting via getDisplayArg()
     * - Shell-special character protection (single-quote wrapping)
     * - Newline splitting (input text can't handle \n; Enter keyevent is sent)
     * - Per-character typing delay when keyboardTypeDelay > 0
     */
    private shellInputText;
    /**
     * yadb (launched via `app_process`) cannot target a specific display and
     * always acts on the default display. When a non-default display is
     * configured, warn so the caller knows the operation lands on the main
     * screen instead of failing silently. Unlike pinch, keyboard operations also
     * have an `input`-based path, so we warn rather than throw.
     */
    private warnYadbOnNonDefaultDisplay;
    getPhysicalDisplayId(): Promise<string | null>;
    private resolvePhysicalDisplayId;
    hideKeyboard(options?: AndroidDeviceInputOpt, timeoutMs?: number): Promise<boolean>;
    private hideKeyboardRaw;
}

declare type AndroidInitArgs = AgentBehaviorInitArgs & {
    deviceId?: string;
    scrcpyVideoBitRate?: number;
    useScrcpy?: boolean;
};

/**
 * Android-specific tools manager
 * Extends BaseMidsceneTools to provide Android ADB device connection tools
 */
export declare class AndroidMidsceneTools extends BaseMidsceneTools<AndroidAgent, AndroidInitArgs> {
    private lastInitArgsSignature?;
    protected getCliReportSessionName(): string;
    protected readonly initArgSpec: InitArgSpec<AndroidInitArgs>;
    protected createTemporaryDevice(): AndroidDevice;
    protected ensureAgent(initArgs?: AndroidInitArgs): Promise<AndroidAgent>;
    /**
     * Provide Android-specific platform tools
     */
    protected preparePlatformTools(): ToolDefinition[];
}

declare type DeviceActionAndroidBackButton = DeviceAction<undefined, void>;

declare type DeviceActionAndroidHomeButton = DeviceAction<undefined, void>;

declare type DeviceActionAndroidRecentAppsButton = DeviceAction<undefined, void>;

declare interface DevicePhysicalInfo {
    physicalWidth: number;
    physicalHeight: number;
    dpr: number;
    orientation: number;
    isCurrentOrientation?: boolean;
}

export declare function getConnectedDevices(deviceOptions?: AndroidDeviceOpt): Promise<Device[]>;

export declare function getConnectedDevicesWithDetails(deviceOptions?: AndroidDeviceOpt): Promise<AndroidConnectedDevice[]>;

export { overrideAIConfig }

/**
 * A raw (not yet decoded) H.264 keyframe emitted by the scrcpy stream.
 * Holding these is cheap — decoding to JPEG costs an ffmpeg run per frame, so
 * consumers (e.g. UI observers) buffer raw keyframes and decode only
 * the frames they actually need, after sampling.
 */
declare interface RawKeyframe {
    /** Raw H.264 keyframe data WITHOUT the SPS/PPS header. */
    data: Buffer;
    /** SPS/PPS header active when this frame was produced (needed to decode). */
    header: Buffer;
    /** Device-monotonic capture timestamp forwarded by scrcpy. */
    ptsUs?: bigint;
    /** Estimated frame age when the packet reached the host. */
    estimatedAgeMs?: number;
    /** Encoder epoch that produced this frame (always set by this manager). */
    streamEpoch?: symbol;
    capturedAt: number;
}

declare type ResolvedScrcpyConfig = Required<ScrcpyConfig>;

/**
 * Resolve a packaged resource path for use by an external process.
 *
 * Electron can read files inside app.asar through Node APIs, but external
 * processes cannot. Electron hosts must configure asarUnpack or unpackDir to
 * extract node_modules/@midscene/android/bin/**; this resolves that unpacked
 * sibling only when it exists.
 */
export declare function resolveExternalResourcePath(resourcePath: string, pathExists?: (path: string) => boolean): string;

export declare type ResolveScrcpyAdbBackend = () => ScrcpyAdbBackend | Promise<ScrcpyAdbBackend>;

declare type RunAdbShellOpt = {
    /**
     * ADB shell command execution timeout in milliseconds.
     */
    timeout?: number;
};

/** ADB capabilities scrcpy needs from the canonical Appium transport. */
export declare interface ScrcpyAdbBackend {
    adbHost?: string;
    adbPort?: number;
    push(localPath: string, remotePath: string): Promise<unknown>;
}

declare type ScrcpyConfig = NonNullable<AndroidDeviceOpt['scrcpyConfig']>;

/**
 * Adapter that encapsulates all scrcpy-related logic for AndroidDevice.
 * Handles config normalization, manager lifecycle, screenshot, and resolution.
 */
export declare class ScrcpyDeviceAdapter {
    private deviceId;
    private scrcpyConfig;
    private resolveAdbBackend;
    private manager;
    private managerPromise;
    private resolvedConfig;
    private lastError;
    private retryAfter;
    private freshnessRestartPromise;
    private lifecycleGeneration;
    private pendingActionBarrierAtHostUs;
    private keyframeListeners;
    private keyframeUnsubscribers;
    constructor(deviceId: string, scrcpyConfig: ScrcpyConfig | undefined, resolveAdbBackend?: ResolveScrcpyAdbBackend);
    isEnabled(): boolean;
    getStatus(): ScrcpyStatus;
    private isConfigured;
    /**
     * Initialize scrcpy connection. Called during device.connect() and explicit retries.
     */
    initialize(deviceInfo: DevicePhysicalInfo): Promise<void>;
    private recordFailure;
    private clearFailure;
    private ensureRetryReady;
    /**
     * Resolve scrcpy config.
     * maxSize defaults to 0 (no scaling, full physical resolution) so the Agent layer
     * receives the highest quality image for AI processing.
     * videoBitRate uses the shared default unless explicitly configured.
     */
    resolveConfig(): ResolvedScrcpyConfig;
    /**
     * @deprecated Device geometry no longer affects scrcpy configuration. Call
     * `resolveConfig()` without arguments.
     */
    resolveConfig(_deviceInfo: DevicePhysicalInfo): ResolvedScrcpyConfig;
    /**
     * Get or create the ScrcpyScreenshotManager.
     * Uses dynamic import for @yume-chan packages (ESM-only, must use await import in CJS builds).
     */
    ensureManager(_deviceInfo: DevicePhysicalInfo): Promise<ScrcpyScreenshotManager>;
    /**
     * Connect the current manager and restore adapter-owned frame subscriptions
     * whenever this call establishes a new stream epoch. Existing connections
     * keep their current subscriptions without churn.
     */
    private ensureConnectedManager;
    /**
     * Take a screenshot via scrcpy, returns base64 string.
     * A stale established stream is restarted once so a static screen can use a
     * fresh frame from the new epoch. Throws only when that retry also fails, so
     * the caller can fall back to ADB.
     */
    screenshotBase64(deviceInfo: DevicePhysicalInfo): Promise<string>;
    private restartAndCaptureOnce;
    private jpegBufferToBase64;
    /**
     * Subscribe to raw keyframes from the scrcpy stream (ensures the stream is
     * connected first). Frames are raw H.264 — no decoding cost. While
     * subscribed, incoming frames keep the connection alive. Returns an
     * unsubscribe function.
     */
    subscribeKeyframes(deviceInfo: DevicePhysicalInfo, listener: (frame: RawKeyframe) => void): Promise<() => void>;
    /** Latest raw keyframe seen on the stream, or null if none yet. */
    getLatestRawKeyframe(): RawKeyframe | null;
    private attachKeyframeListener;
    private attachKeyframeListeners;
    private applyPendingActionBarrier;
    private monotonicTimeUs;
    private deferActionBarrier;
    /**
     * Move the scrcpy PTS barrier past a completed input action. Barrier failures
     * must not turn a successfully injected action into an action error; disable
     * the stream and let subsequent captures use the existing ADB fallback.
     */
    markActionBarrier(): Promise<void>;
    /**
     * Decode a raw keyframe to a JPEG data URL. Deferred, per-frame-expensive
     * step (one ffmpeg process per call) — only call on sampled frames.
     */
    decodeRawKeyframeToJpegBase64(frame: RawKeyframe): Promise<string>;
    /**
     * Get scrcpy's actual video resolution.
     * Returns null if scrcpy is not connected yet.
     */
    getResolution(): {
        width: number;
        height: number;
    } | null;
    /**
     * Compute size from scrcpy resolution.
     * Returns null if scrcpy is not connected.
     */
    getSize(deviceInfo: DevicePhysicalInfo): Size | null;
    /**
     * Calculate the scaling ratio from physical to scrcpy resolution.
     */
    getScalingRatio(physicalWidth: number): number | null;
    disconnect(): Promise<void>;
}

declare interface ScrcpyFreshnessBarrierOptions {
    hostMonotonicUs?: bigint;
}

declare class ScrcpyScreenshotManager {
    private readonly pushServer;
    private adb;
    private scrcpyClient;
    private videoStream;
    private spsHeader;
    private idleTimer;
    private connectionPromise;
    private disposed;
    private isInitialized;
    private options;
    private ffmpegAvailable;
    private keyframeResolvers;
    private keyframeListeners;
    private lastRawKeyframe;
    private lastRawKeyframeAt;
    private lastRawKeyframePtsUs;
    private lastRawKeyframeEstimatedAgeMs;
    private lastRawKeyframeStreamEpoch;
    private streamEpoch;
    private streamReaderEpoch;
    private videoResolution;
    private streamReader;
    private frameFreshnessBarrierPtsUs;
    private frameFreshnessBarrierReason;
    private streamStartupWindow;
    private frameFreshnessBarrierPending;
    private frameFreshnessBarrierGeneration;
    private deviceClockCalibration;
    private deviceClockCalibrationPromise;
    private hasRetriedUncertainClockCalibration;
    private lastFramePtsUs;
    private frameFreshnessError;
    private lastFrameFreshnessWarningAt;
    private hasEstablishedVideoFrame;
    private videoResetState;
    private disposePromise;
    constructor(adb: Adb, pushServer: ScrcpyServerPusher, options?: ScrcpyScreenshotOptions);
    /**
     * Validate environment prerequisites (ffmpeg, scrcpy-server, etc.)
     * Must be called once after construction, before any screenshot operations.
     * Throws if prerequisites are not met.
     */
    validateEnvironment(): Promise<void>;
    /**
     * Ensure scrcpy connection is active
     */
    ensureConnected(): Promise<void>;
    private connectScrcpy;
    private createScrcpyOptions;
    private collectServerOutput;
    private createConnectionError;
    private getErrorOutput;
    /**
     * Resolve path to scrcpy server binary
     */
    private resolveServerBinPath;
    /**
     * Get ffmpeg executable path
     * Priority: @ffmpeg-installer/ffmpeg > system ffmpeg
     */
    private getFfmpegPath;
    /**
     * Consume video frames and keep latest frame
     */
    private startFrameConsumer;
    /**
     * Main frame consumption loop
     * Includes busy-loop detection: if reader.read() resolves too fast
     * (e.g. broken stream returning immediately), we throttle to prevent 100% CPU.
     */
    private consumeFramesLoop;
    /**
     * Process a single video packet from the scrcpy stream.
     * With sendFrameMeta: true, the stream emits properly framed packets:
     * - "configuration" packets contain SPS/PPS header data
     * - "data" packets contain complete video frames with correct boundaries
     * This avoids the frame-splitting issue that occurs with sendFrameMeta: false
     * at high resolutions where raw chunks may not align with frame boundaries.
     */
    private processFrame;
    /**
     * Read the Android uptime clock used by Surface/MediaCodec frame PTS values.
     * The host timestamps bracket the ADB request so frame age can also be
     * estimated on the host wall clock for reports.
     */
    private readDeviceClockCalibration;
    private sampleFrameClockCalibration;
    ensureFrameClockCalibration(): Promise<void>;
    private recalibrateFrameClock;
    /**
     * Invalidate cached frames and require future packets to be captured after
     * the host-monotonic action/planning boundary projected onto the device
     * clock. A caller recovering an unavailable stream can pass the original
     * action-boundary timestamp so connection startup latency does not move the
     * barrier forward. The projection reuses the single calibration for this
     * stream epoch and does not issue another ADB clock read.
     */
    setFreshnessBarrier(reason: string, options?: ScrcpyFreshnessBarrierOptions): Promise<bigint>;
    private isFrameFresh;
    private estimateFrameAgeUs;
    private estimateFrameAge;
    private estimateDeviceTimeUs;
    private getCalibrationUncertaintyUs;
    private isFrameAgeAcceptable;
    private warnFrameFreshness;
    private estimateFrameTiming;
    private clearFrameCache;
    private restoreFrameCache;
    private monotonicTimeUs;
    private advanceStreamEpoch;
    private resetFrameFreshnessState;
    /**
     * Restart scrcpy's capture and encoder pipeline without rebuilding the video
     * stream or the underlying ADB transport. One request is shared by all
     * concurrent capture callers until a post-reset keyframe is accepted.
     */
    private requestVideoReset;
    private waitForVideoResetWrite;
    /**
     * Subscribe to raw keyframes as they arrive from the stream. While at least
     * one subscriber is active, incoming keyframes keep resetting the idle timer
     * so the connection is not torn down mid-capture. Returns an unsubscribe fn.
     *
     * Frames are emitted RAW (no decoding). Use {@link decodeRawKeyframeToJpeg}
     * on the frames you actually need — one ffmpeg run per unique frame.
     */
    subscribeKeyframes(listener: (frame: RawKeyframe) => void): () => void;
    /** Latest raw keyframe seen on the stream, or null if none yet. */
    getLatestRawKeyframe(): RawKeyframe | null;
    private getCachedKeyframeCandidate;
    /**
     * Decode a raw keyframe (from {@link subscribeKeyframes} or
     * {@link getLatestRawKeyframe}) to a JPEG buffer. This is the deferred,
     * per-frame-expensive step (one ffmpeg process per call) — call it only on
     * sampled frames, never inside a capture loop.
     */
    decodeRawKeyframeToJpeg(frame: RawKeyframe): Promise<Buffer>;
    private recordDecodedFrame;
    private isPlanningCandidateFreshEnough;
    private waitForPlanningFrame;
    private createVideoResetFrameTimeoutError;
    private closeStaleStreamAndCreateFallbackError;
    /**
     * Get screenshot as JPEG.
     * A newly connected stream gets a bounded startup window to produce its
     * first frame. All candidates, including that first frame, must satisfy the
     * absolute age limit at capture time. Over-age candidates arm a planning
     * barrier on demand. If no frame crosses the resulting freshness target in
     * time, close this stream epoch and let the caller restart it or use ADB.
     */
    getScreenshotJpeg(): Promise<Buffer>;
    /**
     * Get the actual video stream resolution
     * Returns null if scrcpy is not connected yet
     */
    getResolution(): {
        width: number;
        height: number;
    } | null;
    /**
     * Notify all pending keyframe waiters
     */
    private notifyKeyframeWaiters;
    /**
     * Wait for the next keyframe to arrive
     */
    private waitForNextKeyframe;
    /**
     * Ensure ffmpeg is available for PNG conversion
     */
    private ensureFfmpegAvailable;
    /**
     * Wait for first keyframe with SPS/PPS header
     */
    private waitForKeyframe;
    /**
     * Check if ffmpeg is available in the system
     */
    private checkFfmpegAvailable;
    /**
     * Decode H.264 data to JPEG using ffmpeg
     */
    private decodeH264ToJpeg;
    /**
     * Reset idle timeout timer. While keyframe subscribers are active
     * (e.g. a UIObserver sampling loop), the idle timer is not armed —
     * subscribers are actively consuming the stream. On a static screen
     * with i-frame-interval=0, no new keyframes arrive so processFrame
     * never resets the timer; this guard prevents silent disconnect.
     */
    private resetIdleTimer;
    /**
     * Disconnect scrcpy
     */
    disconnect(): Promise<void>;
    /**
     * Permanently release the scrcpy stream and the owned yume ADB transport.
     * Unlike disconnect(), a disposed manager cannot be reconnected.
     */
    dispose(): Promise<void>;
    /**
     * Check if scrcpy is initialized and connected
     */
    isConnected(): boolean;
}

declare interface ScrcpyScreenshotOptions {
    maxSize?: number;
    videoBitRate?: number;
    idleTimeoutMs?: number;
    videoResetFrameTimeoutMs?: number;
}

/** Transfers the local scrcpy server binary to its device path. */
declare type ScrcpyServerPusher = (localPath: string, remotePath: string) => Promise<void>;

export declare interface ScrcpyStatus {
    enabled: boolean;
    connected: boolean;
    lastError: string | null;
    retryAfter: number | null;
}

declare type ScrollDirection = 'up' | 'down' | 'left' | 'right';

/**
 * Helper type to convert DeviceAction to wrapped method signature
 */
declare type WrappedAction<T extends DeviceAction> = (...args: ActionArgs<T>) => Promise<ActionReturn<T>>;

export { }
