Repository navigation
v0.3.0
0.3.0 keeps the Device panel usable while a device starts. The platform buttons, device picker, and refresh stay available during discovery and boot. If a device is already attached, you can still use its screen and revoke agent access while another starts. Choosing another target cancels the pending attachment; a failed switch keeps the previous device.
In 0.2.0, Android could show a blank panel without an error. The hub announced that its HTTP listener was ready before it started capture. Its default capture path then tried to launch ffmpeg, which could fail with spawn ffmpeg ENOENT. The viewer treated the closed video connection as a pause and showed no error. Android now explicitly uses scrcpy and the device's H.264 encoder, so it no longer needs ffmpeg on the desktop PATH. Startup checks capture readiness, and connection, decoder, and drawing failures appear in the panel. A missing first frame, including after a video reset, fails after 15 seconds instead of waiting indefinitely.
Switching also used to wait for the old attachment to finish its work and close its helpers before showing the new device. Now the new attachment becomes active when it is ready, and the old one cleans up in the background. Already-dispatched work stays bound to the old device; queued input is rejected rather than replayed on the new one. Ownership stays reserved until cleanup completes, so another session cannot take a device that is still being released.
The video path does less work per frame. H.264 packets now share one inspection for the keyframe marker and codec instead of separate scans. A keyframe that begins with an SPS stops inspection at its header. MJPEG parsing returns byte views instead of copying every JPEG, and the viewer decodes only the newest complete frame in each received chunk. If a chunk contains several frames, the older ones no longer cost a bitmap decode and canvas draw.
A local macOS capture-only check against already-running Android emulators and iOS Simulators produced first frames in about 0.8 to 1.9 seconds. This excludes boot, helper downloads, and automation-session setup. It is not a before-and-after benchmark of full device switching, and stopped devices still need time to boot.
Changed
- Panel: Android and iOS have separate platform and device pickers. Each session owns its attachment; tabs and windows showing the same session share that device and its access setting. Android and iOS can run side by side in separate sessions.
- Startup: Device selections use cancellation and latest-selection-wins handling. New targets do not wait for cancelled boots on different devices. A return to the same target waits for the old startup or cleanup to release ownership.
- Discovery: Android resolves running AVD names in batches of four instead of one at a time. Cancelled refreshes stop their native commands rather than leaving discovery processes running.
- Video: Reduce H.264 scans and MJPEG copies, skip outdated frames, and keep the current screen visible while the next device prepares. No extra warm capture processes are kept solely to make switching appear instant.
- Process output: Retain up to 4 MiB per command output stream as byte chunks and decode once at completion. This avoids repeatedly copying large strings and preserves UTF-8 across chunk boundaries.
Fixed
- Android: Fix the silent blank screen when ffmpeg is unavailable by selecting scrcpy explicitly and checking the capture session before reporting startup success.
- Viewer: Report unexpected connection closure, unsupported codecs, decoder setup errors, and unavailable canvases. Refresh can retry a failed viewer without changing the attachment.
- Cancellation: Terminate cancelled command processes and interrupt boot-status and screen-readiness waits. Rollback shuts down only devices started by that operation, never a pre-existing emulator or simulator.
- Pipes and streams: Drain unused helper stdout and stderr so full pipes cannot stall a child. Cancel MJPEG readers and upstream fetches on failure or closure, and release their tracking entries.
- Input: Keep manual tap and swipe tied to the current rendered frame and native stream capability, rather than depending on the automation engine's capability report.
- Privacy: Create private session directories, remove screenshot files on failure as well as success, reject oversized screenshots before reading them, and avoid an extra full-size copy of screenshot bytes.
- Handles: Use one identity check for token, generation, platform, and device ID across the controller, registry, runtime, and server. Forged metadata cannot release a valid attachment.
- Stream proxy: Redact hub credentials from forwarded text, cap upstream WebSocket messages, and reject redirects from the fixed video route.
- Development: Replace the recursive TypeScript shim that exited successfully without checking code with the real pinned TypeScript 6.0.2 compiler. Consolidate overlapping tests while retaining cancellation, ownership, and resource-lifecycle coverage.
Update
Update the plugin pin in your global OpenCode configuration, preserving your other settings:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["github:viralcodex/opencode-demul#v0.3.0"]
}The published plugin exports and tool names are unchanged. Android still needs SDK Platform-Tools, Emulator, and a configured AVD. iOS still needs Xcode and an iOS Simulator runtime on macOS. Both need a supported Node installation with npm; ffmpeg is no longer required for Android video.
Validation includes lint, formatting, the real typecheck, 192 passing tests, bundle generation, and installed-package verification.
Full changelog: v0.2.0...v0.3.0