Repository navigation
Releases
v0.4.0
0.4.0 uses one H.264 viewer for Android emulators and iOS simulators. Android still uses scrcpy and the device encoder. iOS now uses the simulator helper's native H.264 encoder; the proxy converts AVCC records to Annex-B without decoding or re-encoding video on the host.
Changed
- Video buffers stay bounded. After dropping dependent frames, playback waits for a keyframe rather than decoding an incomplete sequence.
- Decoder setup, playback, reset, and terminal disposal have explicit states. Obsolete codec checks and decoder outputs cannot revive a stopped viewer.
- Capture recovery keeps the device handle and automation session attached. If the capture helper exits, the next stream request restarts capture and rotates the viewer proxy's credentials.
- Device ownership records now include reservation, attachment, retirement, rollback, and failed cleanup. Failed cleanup keeps ownership reserved, and retries only unfinished steps.
- Cancelling queued work prevents dispatch. Cancelling running native commands terminates the command before the queue advances; input already sent to the device cannot be undone.
- Native operations have a 120-second deadline. The RPC deadline allows another 15 seconds for cancellation, cleanup, and returning an outcome. Failures retain whether the action may have executed.
Fixed
- A capture helper that exits between readiness and publication is rejected instead of being returned as active.
- Failed initial state publication closes the desktop connection so a fresh connection can retry. Late readiness completion cannot resurrect a closed connection.
- Failed or ended session-event subscriptions reconnect with cancellable backoff, capped at five seconds.
- Cleanup after session deletion or movement remains retryable and does not block another session's cleanup.
- Shutdown intent survives failed cleanup when the retry omits the flag. Explicit
shutdown: falsereleases the attachment without another power-off attempt. Physical-device aliases cannot bypass ownership or cause rollback to shut down another session's device. - Disposal cancels pending discovery and recovery. Destroyed viewers cannot reconnect or retain decode timers.
Overlapping old tests were consolidated into transition tables, and the previous all-in-one desktop test was split into focused cases. Production function complexity is limited to nine. No runtime dependency or FSM framework was added.
Update
Update the plugin pin in your global OpenCode configuration, preserving other settings:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["github:viralcodex/opencode-demul#v0.4.0"]
}Plugin exports and tool names are unchanged. Android still requires SDK Platform-Tools, Emulator, and a configured AVD. iOS requires Xcode and an iOS Simulator runtime on macOS. Both require Node 22.19+ on the 22.x line, or Node 24+, with npm.
Full changelog: v0.3.0...v0.4.0
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
v0.2.0
v0.1.4
What's Changed
- remove detach button and clean readme by @viralcodex in #23
- Clean up dead code and test boilerplate, release v0.1.4 by @viralcodex in #24
Full Changelog: v0.1.3...v0.1.4
v0.1.3
- Harden helper-cache trust checks and lock the complete helper dependency tree.
- Resolve Node and npm consistently in desktop installs, including restricted PATH and Windows.
- Bound process teardown and preserve device ownership when shutdown or cancelled startup fails.
- Detect early emulator exit and block input until a current frame has been drawn.
- Add owner-approved PR validation and verified release packaging.
Full changelog: v0.1.2...v0.1.3
v0.1.2
- Fix Android helper startup in desktop installs by including Android SDK tools in the helper PATH.
- Report backend gesture errors with HTTP status and token-redacted details.
- Reduce redundant tests while retaining regression and integration coverage.
v0.1.1
Fix GitHub plugin installation by shipping prebuilt Android bundles without install-time build hooks. Android source and runtime bundles are unchanged from v0.1.0. No iOS changes are included.
v0.1.0
Emulator plugin for OpenCode Desktop.
Install with github:viralcodex/opencode-demul#v0.1.0.