Skip to main content
Glama
connection.go11 kB
package device import ( "fmt" "log" "net" "os" "os/exec" "path/filepath" "time" "github.com/babelcloud/gbox/packages/cli/internal/util" ) // Note: assets will be embedded at build time using a different approach // ScrcpyConnection handles the actual scrcpy server connection type ScrcpyConnection struct { deviceSerial string scid uint32 adbPath string serverPath string conn net.Conn Listener net.Listener // Made public to match scrcpy-proxy serverCmd *exec.Cmd videoEncoder string // Video encoder preference streamingMode string // Streaming mode (h264, webrtc, mse) } // NewScrcpyConnection creates a new scrcpy connection handler func NewScrcpyConnection(deviceSerial string, scid uint32) *ScrcpyConnection { return NewScrcpyConnectionWithMode(deviceSerial, scid, "webrtc") // Default mode } // NewScrcpyConnectionWithMode creates a new scrcpy connection handler with specific streaming mode func NewScrcpyConnectionWithMode(deviceSerial string, scid uint32, streamingMode string) *ScrcpyConnection { // Find adb path adbPath, err := exec.LookPath("adb") if err != nil { adbPath = "adb" // Fallback to PATH } // Find scrcpy-server.jar serverPath := findScrcpyServerJar() if serverPath == "" { log.Printf("Warning: scrcpy-server.jar not found, will try default location") serverPath = "/data/local/tmp/scrcpy-server.jar" } // Select optimal encoder based on streaming mode videoEncoder := selectVideoEncoder(streamingMode) return &ScrcpyConnection{ deviceSerial: deviceSerial, scid: scid, adbPath: adbPath, serverPath: serverPath, videoEncoder: videoEncoder, streamingMode: streamingMode, } } // selectVideoEncoder chooses the optimal video encoder based on streaming mode func selectVideoEncoder(streamingMode string) string { switch streamingMode { case "h264": // H.264 WebCodecs mode: Use software encoder for maximum compatibility // OMX.google.h264.encoder is the most reliable software encoder return "OMX.google.h264.encoder" case "webrtc", "mse": // WebRTC and MSE modes: use hardware encoder for better performance return "c2.qti.avc.encoder" default: // Default: use hardware encoder return "c2.qti.avc.encoder" } } // Connect establishes connection to scrcpy server on device func (sc *ScrcpyConnection) Connect() (net.Conn, error) { log.Printf("Starting scrcpy connection for device %s on port %d", sc.deviceSerial, sc.scid) // 1. Push server file to device if err := sc.pushServerFile(); err != nil { return nil, fmt.Errorf("failed to push server file: %w", err) } // 2. Setup reverse port forwarding if err := sc.setupReversePortForward(); err != nil { return nil, fmt.Errorf("failed to setup reverse port forward: %w", err) } // 3. Start listener for scrcpy server connection // Try to find an available port starting from scid port := sc.scid maxAttempts := 100 // Try up to 100 different ports var listener net.Listener var err error for i := 0; i < maxAttempts; i++ { listener, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) if err == nil { // Port is available, update scid to match the actual port used if port != sc.scid { log.Printf("Port %d was busy, using port %d instead", sc.scid, port) sc.scid = port } sc.Listener = listener break } // Port is busy, try next one port++ } if sc.Listener == nil { return nil, fmt.Errorf("failed to find available port after %d attempts, starting from port %d", maxAttempts, sc.scid) } // 4. Start scrcpy server on device if err := sc.startScrcpyServer(); err != nil { listener.Close() return nil, fmt.Errorf("failed to start scrcpy server: %w", err) } // 5. Accept connection from scrcpy server log.Printf("Waiting for scrcpy server to connect on port %d...", sc.scid) // Set deadline for accept (extend timeout to 20 seconds for hardware encoder) timeout := 20 * time.Second deadline := time.Now().Add(timeout) if err := listener.(*net.TCPListener).SetDeadline(deadline); err != nil { listener.Close() return nil, fmt.Errorf("failed to set deadline: %w", err) } log.Printf("Listening for scrcpy server connection with %v timeout...", timeout) conn, err := listener.Accept() if err != nil { listener.Close() if netErr, ok := err.(net.Error); ok && netErr.Timeout() { log.Printf("Timeout waiting for scrcpy server connection on port %d", sc.scid) log.Printf("Debug: Check if adb reverse port forward is working...") // Debug: Check reverse port forward status checkCmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "reverse", "--list") if output, err := checkCmd.Output(); err == nil { log.Printf("Debug: Current reverse port forwards:\n%s", string(output)) } // Debug: Check if scrcpy server process is running psCmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "shell", "ps | grep scrcpy") if output, err := psCmd.Output(); err == nil && len(output) > 0 { log.Printf("Debug: Scrcpy server processes found:\n%s", string(output)) } else { log.Printf("Debug: No scrcpy server processes found - server may have crashed") } sc.killScrcpyServer() return nil, fmt.Errorf("timeout waiting for scrcpy server after %v", timeout) } return nil, fmt.Errorf("failed to accept connection: %w", err) } // Clear deadline for future accepts listener.(*net.TCPListener).SetDeadline(time.Time{}) log.Printf("Scrcpy server connected successfully") sc.conn = conn return conn, nil } // pushServerFile pushes scrcpy-server.jar to device func (sc *ScrcpyConnection) pushServerFile() error { // Check if local server file exists if sc.serverPath != "" && sc.serverPath != "/data/local/tmp/scrcpy-server.jar" { // Check if file exists locally if _, err := os.Stat(sc.serverPath); err == nil { log.Printf("Pushing scrcpy-server.jar to device...") cmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "push", sc.serverPath, "/data/local/tmp/scrcpy-server.jar") if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to push server: %s", output) } log.Printf("Server file pushed successfully") } } // Verify server exists on device cmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "shell", "ls", "/data/local/tmp/scrcpy-server.jar") if err := cmd.Run(); err != nil { return fmt.Errorf("scrcpy-server.jar not found on device") } return nil } // setupReversePortForward sets up adb reverse port forwarding func (sc *ScrcpyConnection) setupReversePortForward() error { // Clean up any existing reverse forward cleanCmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "reverse", "--remove", fmt.Sprintf("localabstract:scrcpy_%08x", sc.scid)) cleanCmd.Run() // Ignore error if doesn't exist // Setup new reverse forward log.Printf("Setting up reverse port forward: scrcpy_%08x -> tcp:%d", sc.scid, sc.scid) cmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "reverse", fmt.Sprintf("localabstract:scrcpy_%08x", sc.scid), fmt.Sprintf("tcp:%d", sc.scid)) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to setup reverse forward: %s", output) } return nil } // startScrcpyServer starts the scrcpy server process on device func (sc *ScrcpyConnection) startScrcpyServer() error { // Kill any existing scrcpy server sc.killScrcpyServer() time.Sleep(200 * time.Millisecond) // Build scrcpy server command scidHex := fmt.Sprintf("%08x", sc.scid) // Build command arguments, codec selection depends on streaming mode args := []string{ "-s", sc.deviceSerial, "shell", "CLASSPATH=/data/local/tmp/scrcpy-server.jar", "app_process", "/", "com.genymobile.scrcpy.Server", "3.3.1", // Server version - must match the downloaded jar fmt.Sprintf("scid=%s", scidHex), "video=true", "audio=true", "control=true", "cleanup=true", "log_level=verbose", // Enable verbose logging to debug scroll issues "video_codec_options=i-frame-interval=2", "video_codec=h264", "video_encoder=c2.qti.avc.encoder", } // Select audio codec by mode: // - separated (webm) and webrtc modes: use Opus // - muxed (mp4) mode: use AAC switch sc.streamingMode { case "webm", "webrtc", "mse": // Separated and WebRTC modes use Opus for better WebRTC compatibility args = append(args, "audio_codec=opus", "audio_encoder=c2.android.opus.encoder", ) case "mp4": // Muxed mode uses AAC for MP4 container compatibility args = append(args, "audio_codec=aac", "audio_encoder=c2.android.aac.encoder", ) default: // Default to Opus for unknown modes (better compatibility) args = append(args, "audio_codec=opus", "audio_encoder=c2.android.opus.encoder", ) } cmd := exec.Command(sc.adbPath, args...) log.Printf("Starting scrcpy server with command: %s", cmd.String()) // Start the command sc.serverCmd = cmd // Capture output for debugging cmd.Stdout = util.NewPrefixLogWriter("[scrcpy-out]") cmd.Stderr = util.NewPrefixLogWriter("[scrcpy-err]") if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start scrcpy server: %w", err) } // Give server time to start time.Sleep(500 * time.Millisecond) return nil } // killScrcpyServer kills any running scrcpy server on device func (sc *ScrcpyConnection) killScrcpyServer() { // Kill by process name cmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "shell", "pkill", "-f", "scrcpy.Server") cmd.Run() // Also kill our tracked process if exists if sc.serverCmd != nil && sc.serverCmd.Process != nil { sc.serverCmd.Process.Kill() sc.serverCmd = nil } } // Close closes the scrcpy connection func (sc *ScrcpyConnection) Close() error { log.Printf("Closing scrcpy connection for device %s", sc.deviceSerial) // Close connection if sc.conn != nil { sc.conn.Close() } // Close listener if sc.Listener != nil { sc.Listener.Close() } // Kill server process sc.killScrcpyServer() // Clean up reverse forward cmd := exec.Command(sc.adbPath, "-s", sc.deviceSerial, "reverse", "--remove", fmt.Sprintf("localabstract:scrcpy_%08x", sc.scid)) cmd.Run() return nil } // findScrcpyServerJar finds the scrcpy-server.jar file func findScrcpyServerJar() string { // Note: embedded assets will be handled differently // Fallback to external files locations := []string{ // In project assets directory (primary location) "./assets/scrcpy-server.jar", "../cli/assets/scrcpy-server.jar", "../../packages/cli/assets/scrcpy-server.jar", // In home directory filepath.Join(os.Getenv("HOME"), ".gbox", "scrcpy-server.jar"), // In scrcpy installation "/usr/local/share/scrcpy/scrcpy-server", "/opt/homebrew/share/scrcpy/scrcpy-server", "/usr/share/scrcpy/scrcpy-server", } for _, path := range locations { if _, err := os.Stat(path); err == nil { absPath, _ := filepath.Abs(path) return absPath } } return "" } // Note: embedded server extraction removed - using external files only

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babelcloud/gru-sandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server