UNPKG

videokitten

Version:

A cross-platform Node.js library for recording videos from iOS simulators and Android devices/emulators

5 lines 57.7 kB
{ "version": 3, "sources": ["../src/utils/error-handling.ts", "../src/utils/file-paths.ts", "../src/utils/timeout-signal.ts", "../src/utils/process.ts", "../src/session.ts", "../src/errors.ts", "../src/options/ios.ts", "../src/options/android.ts", "../src/ios.ts", "../src/android.ts", "../src/index.ts"], "sourcesContent": ["import type { OnErrorHandler } from '../options';\n\n/**\n * Handles errors based on the provided error handler strategy\n * @param handler - The error handling strategy ('throw', 'ignore', or a function)\n * @param error - The error to handle\n * @param result - The result to return when not throwing\n * @returns The result if not throwing, otherwise throws the error\n */\nexport function doHandleError(handler: OnErrorHandler | undefined, error: Error): void {\n if (!handler || handler === 'throw') {\n throw error;\n }\n\n if (handler === 'ignore') {\n return;\n }\n\n handler(error);\n}\n", "import crypto from 'node:crypto';\nimport path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\n\n/**\n * Ensures the directory exists for a given file path\n * @param filePath - The file path to ensure directory for\n */\nexport function ensureFileDirectory(filePath: string): void {\n const dir = path.dirname(filePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/**\n * Creates a file path for video recording - handles both files and directories\n * @param platform - The platform ('android' or 'ios')\n * @param extension - The file extension (e.g., 'mp4', 'mov')\n * @param outputPath - Optional output path (file or directory)\n * @returns A complete file path for the video\n */\nexport function createVideoPath(platform: 'android' | 'ios', extension: string, outputPath?: string): string {\n if (!outputPath) {\n const filename = generateVideoFilename(platform, extension);\n return path.join(os.tmpdir(), filename);\n }\n\n // Check if path has any extension - if it does, treat as file; if not, treat as directory\n const parsedPath = path.parse(outputPath);\n const hasExtension = parsedPath.ext.length > 0;\n\n if (hasExtension) {\n // Has extension - treat as file path\n return outputPath;\n } else {\n // No extension - treat as directory, generate filename inside it\n const filename = generateVideoFilename(platform, extension);\n return path.join(outputPath, filename);\n }\n}\n\n/**\n * Generates a unique video filename\n * @param platform - The platform ('android' or 'ios')\n * @param extension - The file extension (e.g., 'mp4', 'mov')\n * @returns A unique filename\n */\nfunction generateVideoFilename(platform: 'android' | 'ios', extension: string): string {\n const timestamp = Date.now();\n const uuid = crypto.randomUUID().slice(0, 8); // Use first 8 chars of UUID for brevity\n return `${platform}-video-${timestamp}-${uuid}.${extension}`;\n}\n", "/**\n * Creates an AbortSignal that combines user signal with timeout\n * @param userSignal - Optional user-provided AbortSignal\n * @param timeoutMs - Optional timeout in milliseconds\n * @returns Combined AbortSignal and cleanup function\n */\nexport function createTimeoutSignal(userSignal?: AbortSignal, timeoutMs?: number): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n // If no timeout and no user signal, return a never-aborting signal\n if (!timeoutMs && !userSignal) {\n return {\n signal: new AbortController().signal,\n cleanup: () => {}\n };\n }\n\n // If only user signal, return it as-is\n if (!timeoutMs && userSignal) {\n return {\n signal: userSignal,\n cleanup: () => {}\n };\n }\n\n // If only timeout, create timeout signal\n if (timeoutMs && !userSignal) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort(new Error(`Recording timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n return {\n signal: controller.signal,\n cleanup: () => clearTimeout(timeoutId)\n };\n }\n\n // Both timeout and user signal - combine them\n const controller = new AbortController();\n let timeoutId: NodeJS.Timeout | undefined;\n\n // Abort if user signal is already aborted\n if (userSignal!.aborted) {\n controller.abort(userSignal!.reason);\n } else {\n // Listen for user signal abortion\n const onUserAbort = () => {\n controller.abort(userSignal!.reason);\n };\n userSignal!.addEventListener('abort', onUserAbort);\n\n // Set up timeout\n timeoutId = setTimeout(() => {\n controller.abort(new Error(`Recording timed out after ${timeoutMs}ms`));\n }, timeoutMs!);\n\n // Clean up user signal listener when our signal is aborted\n controller.signal.addEventListener('abort', () => {\n userSignal!.removeEventListener('abort', onUserAbort);\n });\n }\n\n return {\n signal: controller.signal,\n cleanup: () => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n };\n}\n", "import { spawn, type ChildProcess } from 'node:child_process';\n\nexport type ProcessOptions = {\n command: string;\n args: string[];\n env?: NodeJS.ProcessEnv;\n signal?: AbortSignal;\n readyMatcher?: (data: string) => boolean;\n delay?: number | [number, number];\n};\n\nexport class RecordingProcess {\n private readonly child: ChildProcess;\n private readonly signal?: AbortSignal;\n private stderrBuffer = '';\n private processExited = false;\n private exitCode: number | null = null;\n private exitError?: Error;\n\n constructor(private readonly options: ProcessOptions) {\n this.signal = options.signal;\n this.child = spawn(options.command, options.args, {\n env: options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n this.child.stderr?.on('data', (chunk: Buffer) => {\n this.stderrBuffer += chunk.toString();\n });\n\n this.child.on('exit', (code) => {\n this.processExited = true;\n this.exitCode = code;\n });\n\n this.child.on('error', (err) => {\n this.processExited = true;\n this.exitError = err;\n });\n\n this.signal?.addEventListener('abort', this.onAbort);\n }\n\n async started(): Promise<void> {\n await this.waitForProcessReady();\n await this.sleep(this.startupDelay);\n }\n\n async stop(): Promise<void> {\n await this.sleep(this.stopDelay);\n await this.stopProcess();\n }\n\n private onAbort = () => {\n if (!this.processExited) {\n try {\n this.child.kill('SIGINT');\n } catch {\n // Ignore errors if process is already dead\n }\n }\n };\n\n private get startupDelay(): number {\n return Array.isArray(this.options.delay) ? this.options.delay[0] : this.options.delay ?? 0;\n }\n\n private get stopDelay(): number {\n return Array.isArray(this.options.delay) ? this.options.delay[1] : this.options.delay ?? 0;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n private async waitForProcessReady(): Promise<void> {\n return await new Promise((resolve, reject) => {\n if (this.processExited) {\n return reject(this.getExitError());\n }\n\n const readyMatcher = this.options.readyMatcher;\n let resolved = false;\n\n const resolveOnce = () => {\n if (!resolved) {\n resolved = true;\n this.child.stdout?.removeListener('data', onData);\n this.child.stderr?.removeListener('data', onData);\n this.child.removeListener('spawn', resolveOnce);\n this.child.removeListener('exit', onExit);\n this.child.removeListener('error', onExit);\n resolve();\n }\n };\n\n const onData = (chunk: Buffer) => {\n if (readyMatcher!(chunk.toString())) {\n resolveOnce();\n }\n };\n\n const onExit = () => {\n if (!resolved) {\n reject(this.getExitError());\n }\n };\n\n if (readyMatcher) {\n this.child.stdout?.on('data', onData);\n this.child.stderr?.on('data', onData);\n } else {\n this.child.once('spawn', resolveOnce);\n }\n\n this.child.once('exit', onExit);\n this.child.once('error', onExit);\n\n if (this.signal?.aborted) {\n reject(new Error('Operation was aborted.'));\n }\n });\n }\n\n private async stopProcess(): Promise<void> {\n return await new Promise((resolve, reject) => {\n if (this.processExited) {\n if (this.exitError) {\n return reject(this.exitError);\n } else if (this.exitCode !== 0 && this.exitCode !== null) {\n return reject(this.getExitError());\n }\n return resolve();\n }\n\n this.child.once('exit', () => {\n this.signal?.removeEventListener('abort', this.onAbort);\n resolve();\n });\n\n this.child.once('error', (err) => {\n this.signal?.removeEventListener('abort', this.onAbort);\n reject(err);\n });\n\n try {\n this.child.kill('SIGINT');\n } catch (error) {\n reject(error);\n }\n });\n }\n\n private getExitError(): Error {\n if (this.exitError) {\n return this.exitError;\n }\n\n if (this.signal?.aborted) {\n return new Error('Operation was aborted.');\n }\n\n const message = this.stderrBuffer.trim() || `Process exited with code ${this.exitCode || 'unknown'}`;\n return new Error(message);\n }\n}\n", "import fs from 'node:fs';\n\nimport { VideokittenFileWriteError } from './errors';\nimport type { RecordingProcess } from './utils';\nimport type { OnErrorHandler } from './options';\nimport { doHandleError } from './utils';\n\nexport class RecordingSession {\n constructor(\n private readonly process: RecordingProcess,\n private readonly videoPath: string,\n private readonly onError?: OnErrorHandler\n ) {}\n\n async stop(): Promise<string | undefined> {\n try {\n await this.process.stop();\n\n if (!fs.existsSync(this.videoPath)) {\n throw new VideokittenFileWriteError(\n this.videoPath,\n new Error('Video file was not created')\n );\n }\n return this.videoPath;\n } catch (error) {\n doHandleError(this.onError, error as Error);\n return;\n }\n }\n}\n", "/**\n * Base error class for Videokitten errors\n */\nexport class VideokittenError extends Error {\n constructor(message: string, options?: { cause?: Error }) {\n super(message, options);\n this.name = 'VideokittenError';\n }\n}\n\n/**\n * Error thrown when a device is not found\n */\nexport class VideokittenDeviceNotFoundError extends VideokittenError {\n constructor(deviceId: string, cause?: Error) {\n super(`Device not found: ${deviceId}`, { cause });\n this.name = 'VideokittenDeviceNotFoundError';\n }\n}\n\n/**\n * Error thrown when xcrun tool is not found or not executable\n */\nexport class VideokittenXcrunNotFoundError extends VideokittenError {\n constructor(xcrunPath: string, cause?: Error) {\n super(`xcrun not found at path: ${xcrunPath}`, { cause });\n this.name = 'VideokittenXcrunNotFoundError';\n }\n}\n\n/**\n * Error thrown when adb tool is not found or not executable\n */\nexport class VideokittenAdbNotFoundError extends VideokittenError {\n constructor(adbPath: string, cause?: Error) {\n const troubleshooting = `\nTo fix ADB issues:\n\n1. Check if Android SDK is installed:\n \u2022 Android Studio: Check SDK Manager for Android SDK Platform-Tools\n \u2022 Command line: Look for ANDROID_HOME or ANDROID_SDK_ROOT environment variables\n\n2. Add ADB to your PATH:\n \u2022 macOS/Linux: export PATH=\"$ANDROID_HOME/platform-tools:$PATH\"\n \u2022 Windows: Add %ANDROID_HOME%\\\\platform-tools to your PATH\n\n3. Use adbPath option:\n \u2022 videokitten({ platform: 'android', adbPath: '/path/to/adb' })\n \u2022 Common locations:\n - macOS: ~/Library/Android/sdk/platform-tools/adb\n - Linux: ~/Android/Sdk/platform-tools/adb\n - Windows: %USERPROFILE%\\\\AppData\\\\Local\\\\Android\\\\Sdk\\\\platform-tools\\\\adb.exe\n\n4. Install Android SDK Platform-Tools:\n \u2022 Via Android Studio SDK Manager\n \u2022 Standalone: https://developer.android.com/studio/releases/platform-tools`;\n\n super(`adb not found at path: ${adbPath}${troubleshooting}`, { cause });\n this.name = 'VideokittenAdbNotFoundError';\n }\n}\n\n/**\n * Error thrown when scrcpy tool is not found or not executable\n */\nexport class VideokittenScrcpyNotFoundError extends VideokittenError {\n constructor(scrcpyPath: string, cause?: Error) {\n const installInstructions = `\nTo install scrcpy:\n\n\u2022 macOS: brew install scrcpy\n\u2022 Linux: apt install scrcpy (Ubuntu/Debian) or snap install scrcpy\n\u2022 Windows: winget install scrcpy or download from GitHub releases\n\u2022 From source: https://github.com/Genymobile/scrcpy\n\nMake sure scrcpy is in your PATH or provide the full path via scrcpyPath option.`;\n\n super(`scrcpy not found at path: ${scrcpyPath}${installInstructions}`, { cause });\n this.name = 'VideokittenScrcpyNotFoundError';\n }\n}\n\n/**\n * Error thrown when iOS simulator is not available\n */\nexport class VideokittenIOSSimulatorError extends VideokittenError {\n constructor(deviceId: string, cause?: Error) {\n super(`iOS Simulator not available or not booted: ${deviceId}`, { cause });\n this.name = 'VideokittenIOSSimulatorError';\n }\n}\n\n/**\n * Error thrown when Android device/emulator is not available\n */\nexport class VideokittenAndroidDeviceError extends VideokittenError {\n constructor(deviceId: string, cause?: Error) {\n super(`Android device/emulator not available: ${deviceId}`, { cause });\n this.name = 'VideokittenAndroidDeviceError';\n }\n}\n\n/**\n * Error thrown when video file cannot be written\n */\nexport class VideokittenFileWriteError extends VideokittenError {\n constructor(outputPath: string, cause?: Error) {\n super(`Failed to write video file: ${outputPath}`, { cause });\n this.name = 'VideokittenFileWriteError';\n }\n}\n\n/**\n * Error thrown when operation is aborted\n */\nexport class VideokittenOperationAbortedError extends VideokittenError {\n constructor(operation: string = 'operation') {\n super(`${operation} was aborted`);\n this.name = 'VideokittenOperationAbortedError';\n }\n}\n\n/**\n * Error thrown when video recording command fails with unknown error\n */\nexport class VideokittenRecordingFailedError extends VideokittenError {\n constructor(platform: 'ios' | 'android', cause?: Error) {\n super(`${platform.toUpperCase()} video recording command failed`, { cause });\n this.name = 'VideokittenRecordingFailedError';\n }\n}\n", "import type { VideokittenOptionsBase } from './base';\n\n/** iOS video recording configuration using xcrun simctl io recordVideo */\nexport interface VideokittenOptionsIOS extends VideokittenOptionsBase {\n /** Platform identifier */\n platform: 'ios';\n /** Path to xcrun executable */\n xcrunPath?: string;\n\n /**\n * Specifies the codec type for recording.\n * Maps to --codec\n * @default \"hevc\"\n */\n codec?: 'h264' | 'hevc';\n /**\n * iOS: supports \"internal\" or \"external\". Default is \"internal\".\n * tvOS: supports only \"external\"\n * watchOS: supports only \"internal\"\n * Maps to --display\n * @default \"internal\"\n */\n display?: 'internal' | 'external';\n /**\n * For non-rectangular displays, handle the mask by policy.\n * ignored: The mask is ignored and the unmasked framebuffer is saved.\n * alpha: Not supported, but retained for compatibility; the mask is rendered black.\n * black: The mask is rendered black.\n * Maps to --mask\n */\n mask?: 'ignored' | 'alpha' | 'black';\n /**\n * Force the output file to be written to, even if the file already exists.\n * Maps to --force\n */\n force?: boolean;\n}\n\n/**\n * Creates xcrun simctl command line arguments from iOS options\n * @param options iOS-specific video recording options\n * @returns Array of command line arguments for xcrun simctl io recordVideo\n */\nexport function createIOSOptions(options: Partial<VideokittenOptionsIOS> = {}): string[] {\n const args: string[] = ['simctl', 'io'];\n\n // Add device ID if specified\n if (options.deviceId === undefined) {\n args.push('booted'); // Default to booted simulator\n } else {\n args.push(options.deviceId);\n }\n\n args.push('recordVideo');\n\n // Add iOS-specific options\n if (options.codec) {\n args.push('--codec', options.codec);\n }\n\n if (options.display) {\n args.push('--display', options.display);\n }\n\n if (options.mask) {\n args.push('--mask', options.mask);\n }\n\n if (options.force) {\n args.push('--force');\n }\n\n // Add output path at the end\n if (options.outputPath !== undefined) {\n args.push(options.outputPath);\n }\n\n return args;\n}\n", "import type { VideokittenOptionsBase } from './base';\n\n/** Android-specific video recording options */\nexport interface VideokittenOptionsAndroid extends VideokittenOptionsBase {\n /** Platform identifier */\n platform: 'android';\n /** Path to adb executable */\n adbPath?: string;\n /** Path to scrcpy executable */\n scrcpyPath?: string;\n\n /** Network tunnel configuration for scrcpy */\n tunnel?: ScrcpyTunnelOptions;\n\n /** Window display configuration for scrcpy */\n window?: false | ScrcpyWindowOptions;\n\n /** Video recording configuration */\n recording?: ScrcpyVideoRecordingOptions;\n\n /** Audio recording configuration for scrcpy */\n audio?: false | ScrcpyAudioOptions;\n\n /** Debug and overlay configuration */\n debug?: ScrcpyDebugOptions;\n\n /** Input simulation configuration for scrcpy */\n input?: ScrcpyInputOptions;\n\n /** Android app management */\n app?: ScrcpyAppOptions;\n\n /** Screen management configuration */\n screen?: ScrcpyScreenOptions;\n\n /** Advanced scrcpy configuration options */\n advanced?: ScrcpyAdvancedOptions;\n}\n\n/** Network tunnel configuration for scrcpy remote device connections */\nexport interface ScrcpyTunnelOptions {\n /**\n * Set the IP address of the adb tunnel to reach the scrcpy server.\n * This option automatically enables --force-adb-forward.\n * @default \"localhost\"\n */\n host?: string;\n /**\n * Set the TCP port of the adb tunnel to reach the scrcpy server.\n * This option automatically enables --force-adb-forward.\n * @default 0 (not forced): the local port used for establishing the tunnel will be used\n */\n port?: number;\n}\n\n/** Window display configuration for scrcpy video recording */\nexport interface ScrcpyWindowOptions {\n /**\n * Enable/disable scrcpy window. When disabled, implies --no-video-playback.\n * Maps to --no-window when false\n */\n enabled?: boolean;\n /**\n * Disable window decorations (display borderless window).\n * Maps to --window-borderless\n */\n borderless?: boolean;\n /**\n * Set a custom window title.\n * Maps to --window-title\n */\n title?: string;\n /**\n * Set the initial window horizontal position.\n * Maps to --window-x\n * @default \"auto\"\n */\n x?: number;\n /**\n * Set the initial window vertical position.\n * Maps to --window-y\n * @default \"auto\"\n */\n y?: number;\n /**\n * Set the initial window width.\n * Maps to --window-width\n * @default 0 (automatic)\n */\n width?: number;\n /**\n * Set the initial window height.\n * Maps to --window-height\n * @default 0 (automatic)\n */\n height?: number;\n /**\n * Make scrcpy window always on top (above other windows).\n * Maps to --always-on-top\n */\n alwaysOnTop?: boolean;\n /**\n * Start in fullscreen.\n * Maps to -f, --fullscreen\n */\n fullscreen?: boolean;\n}\n\n/** Video recording quality and format configuration */\nexport interface ScrcpyVideoRecordingOptions {\n /**\n * Encode the video at the given bit rate, expressed in bits/s.\n * Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n * Maps to -b, --video-bit-rate\n * @default 8M (8000000)\n */\n bitRate?: number;\n /**\n * Select a video codec.\n * Maps to --video-codec\n * @default \"h264\"\n */\n codec?: 'h264' | 'h265' | 'av1';\n /**\n * Force recording format.\n * Maps to --record-format\n */\n format?: 'mp4' | 'mkv' | 'm4a' | 'mka' | 'opus' | 'aac' | 'flac' | 'wav';\n\n /**\n * Limit both the width and height of the video to value. The other dimension is computed so that the device aspect-ratio is preserved.\n * Maps to -m, --max-size\n * @default 0 (unlimited)\n */\n maxSize?: number;\n\n /**\n * Crop the device screen on the server.\n * The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).\n * Maps to --crop=width:height:x:y\n */\n crop?: {\n /** Crop width */\n width: number;\n /** Crop height */\n height: number;\n /** Crop X offset */\n x: number;\n /** Crop Y offset */\n y: number;\n };\n\n /**\n * Set the record orientation. The number represents the clockwise rotation in degrees.\n * Maps to --record-orientation or --orientation\n * @default 0\n */\n orientation?: 0 | 90 | 180 | 270;\n\n /**\n * Set the maximum mirroring time, in seconds.\n * Maps to --time-limit\n */\n timeLimit?: number;\n}\n\n/** Audio recording configuration for scrcpy */\nexport interface ScrcpyAudioOptions {\n /**\n * Enable/disable audio forwarding.\n * Maps to --no-audio when false\n */\n enabled?: boolean;\n /**\n * Select the audio source.\n * Maps to --audio-source\n * @default \"output\"\n */\n source?: 'output' | 'playback' | 'mic' | 'mic-unprocessed' | 'mic-camcorder' | 'mic-voice-recognition' | 'mic-voice-communication' | 'voice-call' | 'voice-call-uplink' | 'voice-call-downlink' | 'voice-performance';\n /**\n * Select an audio codec.\n * Maps to --audio-codec\n * @default \"opus\"\n */\n codec?: 'opus' | 'aac' | 'flac' | 'raw';\n /**\n * Encode the audio at the given bit rate, expressed in bits/s.\n * Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n * Maps to --audio-bit-rate\n * @default 128K (128000)\n */\n bitRate?: number;\n /**\n * Configure the audio buffering delay (in milliseconds).\n * Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches).\n * Maps to --audio-buffer\n * @default 50\n */\n buffer?: number;\n /**\n * Use a specific MediaCodec audio encoder (depending on the codec provided by --audio-codec).\n * The available encoders can be listed by --list-encoders.\n * Maps to --audio-encoder\n */\n encoder?: string;\n}\n\n/** Debug and logging configuration for scrcpy */\nexport interface ScrcpyDebugOptions {\n /**\n * Enable \"show touches\" on start, restore the initial value on exit.\n * It only shows physical touches (not clicks from scrcpy).\n * Maps to -t, --show-touches\n */\n showTouches?: boolean;\n /**\n * Set the log level.\n * Maps to -V, --verbosity\n * @default \"info\"\n */\n logLevel?: 'verbose' | 'debug' | 'info' | 'warn' | 'error';\n /**\n * Start FPS counter, to print framerate logs to the console.\n * It can be started or stopped at any time with MOD+i.\n * Maps to --print-fps\n */\n printFps?: boolean;\n}\n\n/** Input simulation configuration for scrcpy */\nexport interface ScrcpyInputOptions {\n /**\n * Select how to send keyboard inputs to the device.\n * Possible values are \"disabled\", \"sdk\", \"uhid\" and \"aoa\".\n * Maps to --keyboard\n */\n keyboard?: 'disabled' | 'sdk' | 'uhid' | 'aoa';\n /**\n * Select how to send mouse inputs to the device.\n * Possible values are \"disabled\", \"sdk\", \"uhid\" and \"aoa\".\n * Maps to --mouse\n */\n mouse?: 'disabled' | 'sdk' | 'uhid' | 'aoa';\n /**\n * Inject key events for all input keys, and ignore text events.\n * Maps to --raw-key-events\n */\n rawKeyEvents?: boolean;\n /**\n * Inject alpha characters and space as text events instead of key events.\n * This avoids issues when combining multiple keys to enter a special character,\n * but breaks the expected behavior of alpha keys in games (typically WASD).\n * Maps to --prefer-text\n */\n preferText?: boolean;\n /**\n * Specify the modifiers to use for scrcpy shortcuts.\n * Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", \"lsuper\" and \"rsuper\".\n * Several shortcut modifiers can be specified.\n * Maps to --shortcut-mod\n * @default [\"lalt\", \"lsuper\"] (left-Alt or left-Super)\n */\n shortcutModifiers?: string[];\n}\n\n/** Android app management configuration for scrcpy */\nexport interface ScrcpyAppOptions {\n /**\n * Package name of app to start.\n * Maps to --start-app\n */\n startApp?: string;\n /**\n * Force stop app before starting (use '+' prefix).\n * Maps to --start-app=+package\n */\n forceStop?: boolean;\n /**\n * Create a new display with specified resolution and density.\n * Maps to --new-display\n */\n newDisplay?: boolean | { width?: number; height?: number; dpi?: number };\n}\n\n/** Android screen management configuration for scrcpy */\nexport interface ScrcpyScreenOptions {\n /**\n * Turn the device screen off immediately.\n * Maps to -S, --turn-screen-off\n */\n turnOff?: boolean;\n /**\n * Set the screen off timeout while scrcpy is running (restore the initial value on exit).\n * Maps to --screen-off-timeout\n */\n timeout?: number;\n}\n\n/** Advanced scrcpy configuration options */\nexport interface ScrcpyAdvancedOptions {\n /**\n * Request SDL to use the given render driver (this is just a hint).\n * Supported names are currently \"direct3d\", \"opengl\", \"opengles2\", \"opengles\", \"metal\" and \"software\".\n * Maps to --render-driver\n */\n renderDriver?: 'direct3d' | 'opengl' | 'opengles2' | 'opengles' | 'metal' | 'software';\n /**\n * Select the video source.\n * Camera mirroring requires Android 12+.\n * Maps to --video-source\n * @default \"display\"\n */\n videoSource?: 'display' | 'camera';\n /**\n * Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.\n * In this mode, adb (USB debugging) is not necessary, and mirroring is disabled.\n * Maps to --otg\n */\n otg?: boolean;\n /**\n * Keep the device on while scrcpy is running, when the device is plugged in.\n * Maps to -w, --stay-awake\n */\n stayAwake?: boolean;\n /**\n * Do not power on the device on start.\n * Maps to --no-power-on when true\n */\n noPowerOn?: boolean;\n /**\n * Kill adb when scrcpy terminates.\n * Maps to --kill-adb-on-close\n */\n killAdbOnClose?: boolean;\n /**\n * Power off the device screen when closing scrcpy.\n * Maps to --power-off-on-close\n */\n powerOffOnClose?: boolean;\n /**\n * Disable screensaver while scrcpy is running.\n * Maps to --disable-screensaver\n */\n disableScreensaver?: boolean;\n}\n\n/**\n * Creates scrcpy command line arguments from Android options\n * @param options Android-specific video recording options\n * @returns Array of command line arguments for scrcpy\n */\nexport function createAndroidOptions(options: Partial<VideokittenOptionsAndroid> = {}): string[] {\n const args: string[] = [];\n\n // Base options\n if (options.deviceId) {\n args.push('--serial', options.deviceId);\n }\n\n if (options.outputPath) {\n args.push('--record', options.outputPath);\n }\n\n // Use unified timeout from base options as the scrcpy time-limit\n if (options.timeout !== undefined) {\n args.push('--time-limit', options.timeout.toString());\n }\n\n // Tunnel options\n if (options.tunnel) {\n if (options.tunnel.host) {\n args.push('--tunnel-host', options.tunnel.host);\n }\n if (options.tunnel.port !== undefined) {\n args.push('--tunnel-port', options.tunnel.port.toString());\n }\n }\n\n // Window options\n if (options.window) {\n if (options.window.enabled === false) {\n args.push('--no-window');\n }\n if (options.window.borderless) {\n args.push('--window-borderless');\n }\n if (options.window.title) {\n args.push('--window-title', options.window.title);\n }\n if (options.window.x !== undefined) {\n args.push('--window-x', options.window.x.toString());\n }\n if (options.window.y !== undefined) {\n args.push('--window-y', options.window.y.toString());\n }\n if (options.window.width !== undefined) {\n args.push('--window-width', options.window.width.toString());\n }\n if (options.window.height !== undefined) {\n args.push('--window-height', options.window.height.toString());\n }\n if (options.window.alwaysOnTop) {\n args.push('--always-on-top');\n }\n if (options.window.fullscreen) {\n args.push('--fullscreen');\n }\n } else if (options.window === false) {\n args.push('--no-window');\n }\n\n // Video recording options\n if (options.recording) {\n if (options.recording.bitRate !== undefined) {\n args.push('--video-bit-rate', options.recording.bitRate.toString());\n }\n if (options.recording.codec) {\n args.push('--video-codec', options.recording.codec);\n }\n if (options.recording.format) {\n args.push('--record-format', options.recording.format);\n }\n if (options.recording.maxSize !== undefined) {\n args.push('--max-size', options.recording.maxSize.toString());\n }\n if (options.recording.crop) {\n const { width, height, x, y } = options.recording.crop;\n args.push('--crop', `${width}:${height}:${x}:${y}`);\n }\n if (options.recording.orientation !== undefined) {\n args.push('--record-orientation', options.recording.orientation.toString());\n }\n // Do not pass duplicate time limit if base timeout is provided\n if (options.recording.timeLimit !== undefined && options.timeout === undefined) {\n args.push('--time-limit', options.recording.timeLimit.toString());\n }\n }\n\n // Audio options\n if (options.audio) {\n if (options.audio.enabled === false) {\n args.push('--no-audio');\n }\n if (options.audio.source) {\n args.push('--audio-source', options.audio.source);\n }\n if (options.audio.codec) {\n args.push('--audio-codec', options.audio.codec);\n }\n if (options.audio.bitRate !== undefined) {\n args.push('--audio-bit-rate', options.audio.bitRate.toString());\n }\n if (options.audio.buffer !== undefined) {\n args.push('--audio-buffer', options.audio.buffer.toString());\n }\n if (options.audio.encoder) {\n args.push('--audio-encoder', options.audio.encoder);\n }\n } else if (options.audio === false) {\n args.push('--no-audio');\n }\n\n // Debug options\n if (options.debug) {\n if (options.debug.showTouches) {\n args.push('--show-touches');\n }\n if (options.debug.logLevel) {\n args.push('--verbosity', options.debug.logLevel);\n }\n if (options.debug.printFps) {\n args.push('--print-fps');\n }\n }\n\n // Input options\n if (options.input) {\n if (options.input.keyboard) {\n args.push('--keyboard', options.input.keyboard);\n }\n if (options.input.mouse) {\n args.push('--mouse', options.input.mouse);\n }\n if (options.input.rawKeyEvents) {\n args.push('--raw-key-events');\n }\n if (options.input.preferText) {\n args.push('--prefer-text');\n }\n if (options.input.shortcutModifiers && options.input.shortcutModifiers.length > 0) {\n args.push('--shortcut-mod', options.input.shortcutModifiers.join(','));\n }\n }\n\n // App options\n if (options.app) {\n if (options.app.startApp) {\n const prefix = options.app.forceStop ? '+' : '';\n args.push('--start-app', `${prefix}${options.app.startApp}`);\n }\n if (options.app.newDisplay) {\n if (typeof options.app.newDisplay === 'boolean' && options.app.newDisplay) {\n args.push('--new-display');\n } else if (typeof options.app.newDisplay === 'object') {\n let displayValue = '';\n if (options.app.newDisplay.width && options.app.newDisplay.height) {\n displayValue = `${options.app.newDisplay.width}x${options.app.newDisplay.height}`;\n }\n if (options.app.newDisplay.dpi) {\n displayValue += `/${options.app.newDisplay.dpi}`;\n }\n if (displayValue) {\n args.push('--new-display', displayValue);\n } else {\n args.push('--new-display');\n }\n }\n }\n }\n\n // Screen options\n if (options.screen) {\n if (options.screen.turnOff) {\n args.push('--turn-screen-off');\n }\n if (options.screen.timeout !== undefined) {\n args.push('--screen-off-timeout', options.screen.timeout.toString());\n }\n }\n\n // Advanced options\n if (options.advanced) {\n if (options.advanced.renderDriver) {\n args.push('--render-driver', options.advanced.renderDriver);\n }\n if (options.advanced.videoSource) {\n args.push('--video-source', options.advanced.videoSource);\n }\n if (options.advanced.otg) {\n args.push('--otg');\n }\n if (options.advanced.stayAwake) {\n args.push('--stay-awake');\n }\n if (options.advanced.noPowerOn) {\n args.push('--no-power-on');\n }\n if (options.advanced.killAdbOnClose) {\n args.push('--kill-adb-on-close');\n }\n if (options.advanced.powerOffOnClose) {\n args.push('--power-off-on-close');\n }\n if (options.advanced.disableScreensaver) {\n args.push('--disable-screensaver');\n }\n }\n\n return args;\n}\n", "import {\n doHandleError,\n createVideoPath,\n ensureFileDirectory,\n createTimeoutSignal,\n RecordingProcess,\n} from './utils';\nimport { RecordingSession } from './session';\nimport type { Videokitten } from './types';\nimport type {\n VideokittenOptionsIOS,\n VideokittenOptionsBase,\n} from './options';\nimport { createIOSOptions } from './options';\nimport {\n VideokittenOperationAbortedError,\n VideokittenXcrunNotFoundError,\n VideokittenIOSSimulatorError,\n VideokittenRecordingFailedError,\n} from './errors';\n\n/**\n * iOS video recording implementation using xcrun simctl\n */\nexport class VideokittenIOS implements Videokitten<VideokittenOptionsIOS> {\n private xcrunPath: string;\n private options: VideokittenOptionsIOS;\n\n constructor(options: VideokittenOptionsIOS) {\n this.options = options;\n this.xcrunPath = options.xcrunPath || '/usr/bin/xcrun';\n }\n\n async startRecording(\n overrideOptions: Partial<VideokittenOptionsBase> = {}\n ): Promise<RecordingSession | undefined> {\n const options = { ...this.options, ...overrideOptions };\n const onError = options.onError || 'throw';\n const expectedPath = createVideoPath('ios', 'mp4', options.outputPath);\n\n // Create unified timeout signal combining user signal and timeout\n const timeoutMs = options.timeout ? options.timeout * 1000 : undefined;\n const { signal, cleanup } = createTimeoutSignal(\n options.abortSignal,\n timeoutMs\n );\n\n try {\n // Ensure the directory exists before recording\n ensureFileDirectory(expectedPath);\n\n // Create command arguments using the options helper\n const args = createIOSOptions({ ...options, outputPath: expectedPath });\n const process = new RecordingProcess({\n command: this.xcrunPath,\n args,\n signal,\n readyMatcher: (data) => data.includes('Recording started'),\n delay: options.delay,\n });\n\n await process.started();\n return new RecordingSession(process, expectedPath, onError);\n } catch (error) {\n const recordingError = this._classifyError(\n error,\n options.deviceId || 'booted'\n );\n doHandleError(onError, recordingError);\n return;\n } finally {\n cleanup();\n }\n }\n\n private _classifyError(error: unknown, deviceId: string): Error {\n if (error instanceof Error) {\n if (error.message.includes('ENOENT') || error.message.includes('command not found')) {\n return new VideokittenXcrunNotFoundError(this.xcrunPath, error);\n } else if (error.message.includes('Invalid device') || error.message.includes('device not found')) {\n return new VideokittenIOSSimulatorError(deviceId, error);\n } else if (\n error.message.includes('aborted') ||\n error.name === 'AbortError'\n ) {\n return new VideokittenOperationAbortedError('iOS video recording');\n } else {\n return new VideokittenRecordingFailedError('ios', error);\n }\n } else {\n return new VideokittenRecordingFailedError('ios');\n }\n }\n}\n", "import {\n VideokittenOperationAbortedError,\n VideokittenScrcpyNotFoundError,\n VideokittenAndroidDeviceError,\n VideokittenFileWriteError,\n VideokittenRecordingFailedError,\n} from './errors';\nimport {\n doHandleError,\n createVideoPath,\n ensureFileDirectory,\n RecordingProcess,\n} from './utils';\nimport { RecordingSession } from './session';\nimport type { Videokitten } from './types';\nimport type {\n VideokittenOptionsAndroid,\n VideokittenOptionsBase,\n} from './options';\nimport { createAndroidOptions } from './options';\n\n/**\n * Gets the appropriate file extension based on the recording format\n * @param format - The recording format from options\n * @returns The file extension (without dot)\n */\nfunction getFileExtension(format?: string): string {\n // scrcpy defaults to mp4 when no format is specified\n return format || 'mp4';\n}\n\n/**\n * Android video recording implementation using scrcpy\n */\nexport class VideokittenAndroid implements Videokitten<VideokittenOptionsAndroid> {\n private scrcpyPath: string;\n private options: VideokittenOptionsAndroid;\n\n constructor(options: VideokittenOptionsAndroid) {\n this.options = { audio: false, window: false, ...options };\n this.scrcpyPath = options.scrcpyPath || 'scrcpy'; // Fallback to PATH\n }\n\n async startRecording(\n overrideOptions: Partial<VideokittenOptionsBase> = {}\n ): Promise<RecordingSession | undefined> {\n const options = { ...this.options, ...overrideOptions };\n const onError = options.onError || 'throw';\n\n // Determine the correct file extension based on recording format\n const extension = getFileExtension(options.recording?.format);\n const expectedPath = createVideoPath(\n 'android',\n extension,\n options.outputPath\n );\n\n try {\n // Check for abort signal before starting\n if (options.abortSignal?.aborted) {\n throw new VideokittenOperationAbortedError('Android video recording');\n }\n\n // Ensure the directory exists before recording\n ensureFileDirectory(expectedPath);\n\n // Create scrcpy command arguments (includes --time-limit if timeout is set)\n const args = createAndroidOptions({ ...options, outputPath: expectedPath });\n\n // Set up environment variables for scrcpy\n const env: NodeJS.ProcessEnv = { ...process.env };\n\n // Set ADB environment variable if adbPath is specified\n // This tells scrcpy where to find the adb executable\n if (options.adbPath) {\n env.ADB = options.adbPath;\n }\n\n const logLevel = options.debug?.logLevel;\n\n const recordingProcess = new RecordingProcess({\n command: this.scrcpyPath,\n args,\n env,\n signal: options.abortSignal,\n delay: options.delay ?? 200,\n readyMatcher:\n (logLevel !== 'warn' && logLevel !== 'error')\n ? (data: string) => data.includes('Device:')\n : undefined,\n });\n\n await recordingProcess.started();\n return new RecordingSession(recordingProcess, expectedPath, onError);\n } catch (error) {\n const recordingError = this._classifyError(\n error,\n options.deviceId || 'default',\n expectedPath\n );\n doHandleError(onError, recordingError);\n return;\n }\n }\n\n private _classifyError(error: unknown, deviceId: string, outputPath: string): Error {\n if (error instanceof Error) {\n if (error.message.includes('ENOENT') || error.message.includes('command not found')) {\n return new VideokittenScrcpyNotFoundError(this.scrcpyPath, error);\n } else if (error.message.includes('device not found') || error.message.includes('device offline')) {\n return new VideokittenAndroidDeviceError(deviceId, error);\n } else if (error.message.includes('aborted') || error.name === 'AbortError') {\n return new VideokittenOperationAbortedError('Android video recording');\n } else if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'EACCES') {\n return new VideokittenFileWriteError(outputPath, error);\n } else {\n return new VideokittenRecordingFailedError('android', error);\n }\n } else {\n return new VideokittenRecordingFailedError('android');\n }\n }\n}\n", "import type { VideokittenOptions } from './types';\nimport { VideokittenIOS } from './ios';\nimport { VideokittenAndroid } from './android';\n\n/**\n * Create a Videokitten instance based on the provided options.\n */\nexport function videokitten(options: VideokittenOptions) {\n switch (options.platform) {\n case 'ios': {\n return new VideokittenIOS(options);\n }\n case 'android': {\n return new VideokittenAndroid(options);\n }\n default: {\n throw new Error(`Unsupported platform: ${(options as any).platform}`);\n }\n }\n}\n\n// Core types that users need\nexport type { Videokitten, VideokittenOptions } from './types';\nexport type {\n VideokittenOptionsIOS,\n VideokittenOptionsAndroid,\n OnErrorHandler\n} from './options';\n\n// Public error classes - users can catch these to handle specifically videokitten errors\nexport {\n VideokittenError,\n VideokittenOperationAbortedError,\n VideokittenFileWriteError,\n VideokittenDeviceNotFoundError,\n VideokittenXcrunNotFoundError,\n VideokittenScrcpyNotFoundError,\n VideokittenAdbNotFoundError,\n VideokittenIOSSimulatorError,\n VideokittenAndroidDeviceError,\n VideokittenRecordingFailedError,\n} from './errors';\nexport type { RecordingSession } from './session';\n"], "mappings": "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