@8bitbish/screenshot-service
v1.2.3
Published
Capture screenshots and recordings from connected iPhone and Android devices, with a built-in visual setup wizard.
Downloads
1,222
Maintainers
Readme
@8bitbish/screenshot-service
Take screenshots and fetch screen recordings from a connected iPhone or Android device, straight to your Mac as a Buffer. Works over USB or WiFi. No cloud, no Shortcuts, no tapping the phone.
Install
npm install @8bitbish/screenshot-serviceOne-time machine setup
Each computer needs Python + pymobiledevice3 (for iOS) and/or ADB (for Android). Run the relevant setup once per machine:
npx @8bitbish/screenshot-service setup-ios # iOS
npx @8bitbish/screenshot-service setup-android # AndroidThese walk you through everything: Homebrew installs, device trust/pairing, and (for iOS) an optional passwordless-sudo grant for the tunnel daemon. Installing the npm package itself is silent and side-effect-free — no prompts on npm install.
Test it:
npx @8bitbish/screenshot-service test-ios
npx @8bitbish/screenshot-service test-androidUsage
Listing connected devices (1.1.0+)
import { listDevices } from '@8bitbish/screenshot-service'
const devices = await listDevices()
// [
// { id: '00008110-...', platform: 'ios', name: 'iPhone 14', nickname: "Jake's iPhone", model: 'iPhone14,7', softwareVersion: 'iOS 26.4.2', available: true },
// { id: 'R5CW13B2FWT', platform: 'android', name: 'Galaxy S23 Ultra', nickname: "Jake's S23 Ultra", model: 'SM-S918B', softwareVersion: 'One UI 8.0', available: true },
// ]
// In your UI, you can show whichever you want:
const label = device.nickname ?? device.name // prefer the user's chosen name
const both = `${device.name} (${device.nickname})` // or show bothnickname is the name the owner set in their phone's Settings ("Jake's iPhone"); name is the friendly model ("iPhone 14"). Apps decide which to display.
Use device.id to target a specific device in capture calls. Duplicate ADB endpoints
that point to the same physical phone are deduped to a single entry.
Capturing
import {
triggerPhoneScreenshot,
triggerAndroidScreenshot,
fetchLatestPhoneRecording,
fetchLatestAndroidRecording,
startTunnel,
} from '@8bitbish/screenshot-service'
import * as fs from 'fs'
// iOS — start tunnel once at app launch (no-op if already running)
await startTunnel()
const ios = await triggerPhoneScreenshot({ includeLocation: true })
fs.writeFileSync('iphone.png', ios.image)
// ios.device.{model, name, iosVersion}
// ios.foregroundApp.{bundleId, name, version} // null if undetected
// ios.location.{city, country, latitude, longitude} // null unless includeLocation
// Android — no tunnel needed
const android = await triggerAndroidScreenshot({ includeLocation: true })
fs.writeFileSync('android.png', android.image)
// Target a specific device by id from listDevices()
const ofGalaxy = await triggerAndroidScreenshot({ deviceId: 'R5CW13B2FWT' })
const ofiPhone = await triggerPhoneScreenshot({ deviceId: '00008110-...' })
// android.device.{model, name, androidVersion, softwareVersion}
// same foregroundApp + location shape as iOS
// Fetch the latest video recording (user records on device, then call this)
const rec = await fetchLatestPhoneRecording({ waitForNew: true, waitTimeout: 120_000 })
fs.writeFileSync('recording.mov', rec.video)API
listDevices(): Promise<ConnectedDevice[]>
// returns { id, platform: 'ios'|'android', name, model, softwareVersion, available, unavailableReason? }
// — duplicate ADB endpoints to the same phone are deduped.
triggerPhoneScreenshot(opts?: {
timeout?: number // default 30000
includeLocation?: boolean // default false
deviceId?: string // iOS UDID from listDevices(); default: first connected
}): Promise<ScreenshotResult>
triggerAndroidScreenshot(opts?: {
timeout?: number
includeLocation?: boolean
deviceId?: string // Android ro.serialno from listDevices(); default: best available (awake preferred)
}): Promise<AndroidScreenshotResult>
fetchLatestPhoneRecording(opts?: {
waitForNew?: boolean // poll until a new video appears (user records then stops)
waitTimeout?: number // default 300000 (5 min)
timeout?: number
includeLocation?: boolean
deviceId?: string // iOS UDID; default: first connected
}): Promise<PhoneRecordingResult>
fetchLatestAndroidRecording(opts?: {
waitForNew?: boolean
waitTimeout?: number
timeout?: number
includeLocation?: boolean
deviceId?: string // Android ro.serialno; default: best available
}): Promise<AndroidRecordingResult>
// iOS tunnel control (Android needs no tunnel — ADB handles it)
startTunnel(): Promise<void> // requires sudo; no-op if already running
isTunnelRunning(): boolean
stopTunnel(): voidAll result objects share the shape:
{
image?: Buffer // present on screenshot results
video?: Buffer // present on recording results
device: { ...platform-specific fields }
foregroundApp: { bundleId, name, version } | null
capturedAt: Date
location: { city, country, latitude, longitude, source } | null
}How it works
iOS — A background tunnel (pymobiledevice3 remote tunneld) connects Mac ↔ iPhone over Apple's RemoteXPC/RSD protocol (iOS 17+). The tunnel needs root; setup-ios offers a passwordless-sudo rule for just this command so you're not prompted every session. Screenshots use the DVT screenshot service; recordings are pulled directly from the Camera Roll via AFC.
Android — Uses ADB. Screenshots via adb exec-out screencap -p. Recordings are pulled from MediaStore via adb pull. Wireless connections auto-reconnect to the last known IP, so a Mac reboot doesn't break things (as long as Wireless Debugging is still toggled ON on the phone).
Troubleshooting
| Symptom | Fix |
|---|---|
| Python 3.11+ with pymobiledevice3 not found | npx @8bitbish/screenshot-service setup-ios |
| adb not found | npx @8bitbish/screenshot-service setup-android |
| No Android device found after Mac reboot | The package auto-reconnects to the last wireless IP. If it still fails, check Settings → Developer Options → Wireless Debugging is ON on the phone. |
| iOS screenshot fails repeatedly | Make sure the iPhone is unlocked and trusted with this Mac |
| Tunnel dies after Mac sleeps | Next triggerPhoneScreenshot() call restarts it automatically |
Requirements
- macOS (uses Homebrew, AFC, ADB)
- Node.js 18+
- iOS 17+ for iPhone support
- Android 10+ for Android support (Wireless Debugging requires Android 11+)
