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:videochannel and feeds them into WebCodecsVideoDecoderinstances - Audio Handler: Processes Opus or AAC frames from
scrcpy:audiothrough the Web Audio API - Control Handler: Manages the
scrcpy:controlchannel for injecting touch events, key presses, and scroll gestures - Clipboard Handler: Handles asynchronous clipboard synchronization via
scrcpy:clipboardwhenclipboardAutosyncis 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:
- Initializes the ByteBridge connection
- Maintains the lifecycle of the scrcpy session
- Ensures proper cleanup of streams and processes when the session terminates
- 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 rendererbitRate: Controls the H.264 encoding bitrate for network efficiencytunnelForward: Determines whether to use ADB reverse tunneling or forward connectionsclipboardAutosync: 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.tsdispatches MessagePort messages to appropriate handlers - Session Management in
service/session.tsenforces single-ownership lifecycle control - Options Builder in
src/options.tsadapts 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →