How the Scrcpy Protocol Is Implemented in @escrcpy/wscrcpy: A Deep Dive

The @escrcpy/wscrcpy package bridges the native scrcpy protocol to the Electron-Vue frontend by splitting implementation between a main-process ByteBridge that pumps TCP streams through MessagePort and a renderer-process protocol stack that decodes video, audio, and control frames via WebCodecs and Web Audio.

The @escrcpy/wscrcpy package serves as the core protocol layer in the viarotel-org/escrcpy repository, translating binary scrcpy streams from Android devices into web-compatible formats. This implementation adapts the original scrcpy protocol—which relies on ADB-forwarded TCP connections for video, audio, control, and clipboard synchronization—into a browser-friendly architecture using Electron's multi-process model and modern web APIs.

Architecture Overview

The implementation strictly separates concerns between Electron's main process and renderer process to handle the binary nature of the scrcpy protocol safely.

Main Process Responsibilities

The main process manages raw socket connections to Android devices. It uses the @yume-chan/* library suite to establish ADB connections and negotiate scrcpy streams. The ByteBridge factory located in packages/wscrcpy/service/bridge/ creates a thin pump that forwards raw byte chunks from TCP sockets into MessagePort messages destined for the renderer. This isolates risky binary operations from the browser context.

Renderer Process Responsibilities

The renderer consumes MessagePort streams and handles all protocol decoding. Located in packages/wscrcpy/src/stack/, the protocol stack parses scrcpy binary frames for separate channels including scrcpy:video, scrcpy:audio, scrcpy:control, and scrcpy:clipboard. Video and audio decoding leverage the WebCodecs API and Web Audio respectively, while input events serialize back into the scrcpy binary format for transmission.

Main Process Implementation: ByteBridge and TCP Transport

The main-process implementation centers on the ByteBridge pattern found in packages/wscrcpy/service/bridge/. This module exports a factory function that instantiates bridge instances for each device connection.

The ByteBridge performs three critical functions:

  • Opens a TCP socket to the Android device via ADB forward
  • Translates the continuous binary scrcpy stream into discrete MessagePort messages
  • Forwards these messages to the specific renderer process that owns the session

The implementation ensures that only one session per window owns the device connection, enforced by the WscrcpySession class in packages/wscrcpy/service/session.ts.

Renderer Process Protocol Stack

The renderer side implements the full scrcpy protocol stack in packages/wscrcpy/src/stack/. This directory contains specialized handlers for each scrcpy channel:

  • Video Handler: Parses H.264 NAL units from the scrcpy:video channel and feeds them into WebCodecs VideoDecoder instances
  • Audio Handler: Processes Opus or AAC frames from scrcpy:audio through the Web Audio API
  • Control Handler: Manages the scrcpy:control channel for injecting touch events, key presses, and scroll gestures
  • Clipboard Handler: Handles asynchronous clipboard synchronization via scrcpy:clipboard when clipboardAutosync is enabled

Each handler re-assembles binary frames from the MessagePort chunks and publishes typed events that the Vue application consumes.

Runtime Routing and Session Management

The packages/wscrcpy/src/core/runtime.ts file serves as the central router for all wscrcpy channels. It receives MessagePort messages from the main process and dispatches them to the appropriate stack layer based on the channel identifier.

Session ownership is strictly managed by the WscrcpySession class in packages/wscrcpy/service/session.ts. This class:

  1. Initializes the ByteBridge connection
  2. Maintains the lifecycle of the scrcpy session
  3. Ensures proper cleanup of streams and processes when the session terminates
  4. Prevents multiple simultaneous connections to the same device from a single window

Type definitions across the package are centralized in packages/wscrcpy/shared/types.ts, which exports interfaces like DeviceTarget and channel name constants used throughout the stack.

Configuration and Options

The packages/wscrcpy/src/options.ts file builds scrcpy command-line arguments and injects sensible defaults for the Electron environment. Key configurations include:

  • maxSize: Limits the video resolution to prevent memory issues in the renderer
  • bitRate: Controls the H.264 encoding bitrate for network efficiency
  • tunnelForward: Determines whether to use ADB reverse tunneling or forward connections
  • clipboardAutosync: Enables automatic clipboard synchronization between desktop and device

These options default to keeping audio opt-in and avoiding clipboard-induced control stalls, adapting the standard scrcpy behavior for GUI applications.

Practical Implementation Examples

Using the Wscrcpy Vue Component

The simplest integration uses the high-level Wscrcpy component, which encapsulates the entire protocol stack:

<!-- src/views/mirror/Screen.vue -->
<template>
  <Wscrcpy
    :device="deviceId"
    :options="scrcpyOptions"
    @ready="onReady"
    @error="onError"
  />
</template>

<script setup>
import { ref } from 'vue'
import Wscrcpy from '@escrcpy/wscrcpy'

const deviceId = ref('emulator-5554')
const scrcpyOptions = { 
  maxSize: 720, 
  bitRate: 2_000_000,
  tunnelForward: true 
}

const onReady = () => console.log('scrcpy protocol connected')
const onError = err => console.error('scrcpy protocol error', err)
</script>

Accessing Streams via Composables

For fine-grained control over the scrcpy protocol streams, use the useWscrcpyConnection composable:

// src/hooks/useDeviceMirror.ts
import { useWscrcpyConnection } from '@escrcpy/wscrcpy'

const { video, audio, control, clipboard } = useWscrcpyConnection({
  device: 'emulator-5554',
  options: { 
    maxSize: 1080,
    clipboardAutosync: true 
  }
})

video.onFrame((frame: VideoFrame) => {
  // frame is a decoded VideoFrame from WebCodecs
  console.log('scrcpy video frame received', frame)
})

control.sendTouch({ x: 100, y: 200, pressure: 1.0 })

Programmatic Session Control

Direct access to the scrcpy protocol lifecycle is available through the WscrcpySession API:

import { WscrcpySession } from '@escrcpy/wscrcpy/service'

const session = new WscrcpySession('emulator-5554')

await session.start({
  maxSize: 720,
  bitRate: 8000000
})

// Session runs with full protocol stack active
console.log('ByteBridge connected, protocol stack initialized')

await session.stop()
// Cleans up MessagePort, TCP socket, and decoders

Summary

The @escrcpy/wscrcpy implementation of the scrcpy protocol follows a strict separation between main and renderer processes:

  • ByteBridge in service/bridge/ handles raw TCP to MessagePort translation in the main process
  • Protocol Stack in src/stack/ decodes video, audio, control, and clipboard channels in the renderer
  • Runtime Router in src/core/runtime.ts dispatches MessagePort messages to appropriate handlers
  • Session Management in service/session.ts enforces single-ownership lifecycle control
  • Options Builder in src/options.ts adapts scrcpy CLI arguments for safe GUI defaults

This architecture allows the Escrcpy application to leverage the full performance of the scrcpy protocol while maintaining the security and compatibility constraints of a Chromium-based environment.

Frequently Asked Questions

What is ByteBridge in @escrcpy/wscrcpy?

ByteBridge is a factory-created transport layer in packages/wscrcpy/service/bridge/ that connects raw TCP sockets from ADB-forwarded scrcpy streams to Electron's MessagePort API. It runs exclusively in the main process and forwards binary chunks from the Android device to the renderer process without parsing the protocol, ensuring that risky socket operations remain outside the browser sandbox.

How does @escrcpy/wscrcpy handle video decoding?

Video frames are decoded using the WebCodecs API in the renderer process. The scrcpy:video channel handler in packages/wscrcpy/src/stack/ receives H.264 NAL units via MessagePort, assembles complete frames, and feeds them into VideoDecoder instances. This approach provides hardware-accelerated decoding directly in the browser window without requiring native video players.

What is the difference between the main process and renderer process implementation?

The main process manages network transport and binary sockets, while the renderer process handles protocol parsing and media decoding. The main process uses @yume-chan libraries to establish ADB connections and ByteBridge to pump bytes, whereas the renderer implements the scrcpy protocol stack including scrcpy:video, scrcpy:audio, scrcpy:control, and scrcpy:clipboard handlers that translate binary frames into JavaScript events and WebCodecs frames.

How are scrcpy command-line options configured in the package?

Options are centralized in packages/wscrcpy/src/options.ts, which constructs scrcpy arguments and sets Electron-safe defaults. The options builder handles parameters like maxSize, bitRate, tunnelForward, and clipboardAutosync, automatically applying values that prevent common issues such as audio synchronization stalls or excessive memory usage in renderer processes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →