Android-SurfaceView-投屏-例子

我来为您提供一个完整的Android SurfaceView投屏Java示例,包括发送端和接收端。

发送端(Sender)完整示例

复制代码
// MainActivity.java - 发送端主界面
public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_CODE = 1;
    private MediaProjectionManager mProjectionManager;
    private Button mStartBtn;
    private ScreenEncoder mScreenEncoder;
    private SocketClient mSocketClient;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        mStartBtn = findViewById(R.id.btn_start);
        mStartBtn.setOnClickListener(v -> startScreenCapture());
        
        // 初始化Socket客户端(连接到接收端IP)
        mSocketClient = new SocketClient("192.168.1.100", 8888);
        mSocketClient.connect();
    }
    
    private void startScreenCapture() {
        Intent intent = mProjectionManager.createScreenCaptureIntent();
        startActivityForResult(intent, REQUEST_CODE);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
            MediaProjection mediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
            startEncoding(mediaProjection);
        }
    }
    
    private void startEncoding(MediaProjection mediaProjection) {
        mScreenEncoder = new ScreenEncoder(this, mediaProjection, mSocketClient);
        mScreenEncoder.start();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mScreenEncoder != null) {
            mScreenEncoder.stop();
        }
        if (mSocketClient != null) {
            mSocketClient.disconnect();
        }
    }
}

// ScreenEncoder.java - 屏幕编码器
public class ScreenEncoder {
    private static final String MIME_TYPE = "video/avc";
    private static final int FRAME_RATE = 30;
    private static final int IFRAME_INTERVAL = 5;
    
    private Context mContext;
    private MediaProjection mMediaProjection;
    private VirtualDisplay mVirtualDisplay;
    private MediaCodec mEncoder;
    private SocketClient mSocketClient;
    private Thread mEncodeThread;
    
    public ScreenEncoder(Context context, MediaProjection mediaProjection, SocketClient socketClient) {
        mContext = context;
        mMediaProjection = mediaProjection;
        mSocketClient = socketClient;
    }
    
    public void start() {
        try {
            // 获取屏幕尺寸
            DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
            int width = metrics.widthPixels;
            int height = metrics.heightPixels;
            int density = metrics.densityDpi;
            
            // 配置编码器
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            format.setInteger(MediaFormat.KEY_BIT_RATE, 5000000);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
            
            mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
            mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            
            // 获取编码器输入Surface
            Surface inputSurface = mEncoder.createInputSurface();
            mEncoder.start();
            
            // 创建虚拟显示
            mVirtualDisplay = mMediaProjection.createVirtualDisplay(
                "ScreenCapture",
                width, height, density,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                inputSurface, null, null);
            
            // 启动编码线程
            mEncodeThread = new Thread(this::encodeLoop);
            mEncodeThread.start();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private void encodeLoop() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 获取编码输出
                int outputBufferId = mEncoder.dequeueOutputBuffer(bufferInfo, 10000);
                if (outputBufferId >= 0) {
                    ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferId);
                    if (outputBuffer != null && bufferInfo.size > 0) {
                        // 发送编码数据
                        byte[] data = new byte[bufferInfo.size];
                        outputBuffer.get(data);
                        mSocketClient.sendData(data);
                    }
                    mEncoder.releaseOutputBuffer(outputBufferId, false);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public void stop() {
        if (mEncodeThread != null) {
            mEncodeThread.interrupt();
            mEncodeThread = null;
        }
        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
            mVirtualDisplay = null;
        }
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
    }
}

// SocketClient.java - Socket客户端
public class SocketClient {
    private String mServerIp;
    private int mServerPort;
    private Socket mSocket;
    private OutputStream mOutputStream;
    
    public SocketClient(String serverIp, int serverPort) {
        mServerIp = serverIp;
        mServerPort = serverPort;
    }
    
    public void connect() {
        new Thread(() -> {
            try {
                mSocket = new Socket(mServerIp, mServerPort);
                mOutputStream = mSocket.getOutputStream();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
    
    public void sendData(byte[] data) {
        if (mOutputStream != null) {
            try {
                // 添加数据长度头
                ByteBuffer buffer = ByteBuffer.allocate(4 + data.length);
                buffer.putInt(data.length);
                buffer.put(data);
                mOutputStream.write(buffer.array());
                mOutputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void disconnect() {
        try {
            if (mOutputStream != null) {
                mOutputStream.close();
            }
            if (mSocket != null) {
                mSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

接收端(Receiver)完整示例

复制代码
// MainActivity.java - 接收端主界面
public class MainActivity extends AppCompatActivity {
    private SurfaceView mSurfaceView;
    private ScreenDecoder mScreenDecoder;
    private SocketServer mSocketServer;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mSurfaceView = findViewById(R.id.surface_view);
        mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {
                startDecoding(holder.getSurface());
            }
            
            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}
            
            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
                stopDecoding();
            }
        });
        
        // 启动Socket服务器
        mSocketServer = new SocketServer(8888);
        mSocketServer.start();
    }
    
    private void startDecoding(Surface surface) {
        mScreenDecoder = new ScreenDecoder(surface, mSocketServer);
        mScreenDecoder.start();
    }
    
    private void stopDecoding() {
        if (mScreenDecoder != null) {
            mScreenDecoder.stop();
            mScreenDecoder = null;
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mSocketServer != null) {
            mSocketServer.stop();
        }
    }
}

// ScreenDecoder.java - 屏幕解码器
public class ScreenDecoder {
    private static final String MIME_TYPE = "video/avc";
    
    private Surface mSurface;
    private MediaCodec mDecoder;
    private SocketServer mSocketServer;
    private Thread mDecodeThread;
    
    public ScreenDecoder(Surface surface, SocketServer socketServer) {
        mSurface = surface;
        mSocketServer = socketServer;
    }
    
    public void start() {
        try {
            // 配置解码器(需要与发送端分辨率匹配)
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, 1080, 1920);
            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920 * 1080);
            
            mDecoder = MediaCodec.createDecoderByType(MIME_TYPE);
            mDecoder.configure(format, mSurface, null, 0);
            mDecoder.start();
            
            // 启动解码线程
            mDecodeThread = new Thread(this::decodeLoop);
            mDecodeThread.start();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private void decodeLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 从Socket服务器获取数据
                byte[] data = mSocketServer.getData();
                if (data != null && data.length > 0) {
                    // 解码数据
                    int inputBufferId = mDecoder.dequeueInputBuffer(10000);
                    if (inputBufferId >= 0) {
                        ByteBuffer inputBuffer = mDecoder.getInputBuffer(inputBufferId);
                        if (inputBuffer != null) {
                            inputBuffer.put(data);
                            mDecoder.queueInputBuffer(inputBufferId, 0, data.length, 
                                System.nanoTime() / 1000, 0);
                        }
                    }
                    
                    // 处理解码输出
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    int outputBufferId = mDecoder.dequeueOutputBuffer(bufferInfo, 10000);
                    if (outputBufferId >= 0) {
                        mDecoder.releaseOutputBuffer(outputBufferId, true);
                    }
                }
                
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public void stop() {
        if (mDecodeThread != null) {
            mDecodeThread.interrupt();
            mDecodeThread = null;
        }
        if (mDecoder != null) {
            mDecoder.stop();
            mDecoder.release();
            mDecoder = null;
        }
    }
}

// SocketServer.java - Socket服务器
public class SocketServer {
    private int mPort;
    private ServerSocket mServerSocket;
    private Socket mClientSocket;
    private InputStream mInputStream;
    private Thread mServerThread;
    private BlockingQueue<byte[]> mDataQueue = new LinkedBlockingQueue<>();
    
    public SocketServer(int port) {
        mPort = port;
    }
    
    public void start() {
        mServerThread = new Thread(() -> {
            try {
                mServerSocket = new ServerSocket(mPort);
                mClientSocket = mServerSocket.accept();
                mInputStream = mClientSocket.getInputStream();
                
                // 持续接收数据
                byte[] lengthBytes = new byte[4];
                while (!Thread.currentThread().isInterrupted()) {
                    // 读取数据长度
                    int read = mInputStream.read(lengthBytes);
                    if (read == 4) {
                        int dataLength = ByteBuffer.wrap(lengthBytes).getInt();
                        byte[] data = new byte[dataLength];
                        
                        // 读取实际数据
                        int totalRead = 0;
                        while (totalRead < dataLength) {
                            int bytesRead = mInputStream.read(data, totalRead, dataLength - totalRead);
                            if (bytesRead == -1) break;
                            totalRead += bytesRead;
                        }
                        
                        if (totalRead == dataLength) {
                            mDataQueue.put(data);
                        }
                    }
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        });
        mServerThread.start();
    }
    
    public byte[] getData() {
        try {
            return mDataQueue.poll(100, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            return null;
        }
    }
    
    public void stop() {
        if (mServerThread != null) {
            mServerThread.interrupt();
            mServerThread = null;
        }
        try {
            if (mInputStream != null) mInputStream.close();
            if (mClientSocket != null) mClientSocket.close();
            if (mServerSocket != null) mServerSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

布局文件

复制代码
发送端布局(activity_main_sender.xml):


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
    
    <Button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始投屏" />
        
</LinearLayout>


接收端布局(activity_main_receiver.xml):

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        
</FrameLayout>

权限配置

复制代码
AndroidManifest.xml(发送端):
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />

<service
    android:name=".ScreenCaptureService"
    android:foregroundServiceType="mediaProjection" />


AndroidManifest.xml(接收端):
<uses-permission android:name="android.permission.INTERNET" />

使用说明

  1. 发送端:

    • 修改SocketClient中的IP地址为接收端设备的实际IP

    • 点击"开始投屏"按钮请求录屏权限

    • 系统会自动开始编码并发送数据

  2. 接收端:

    • 确保设备在同一局域网

    • 应用启动后自动监听8888端口

    • SurfaceView创建后开始接收并解码显示

  3. 关键点:

    • 发送端使用MediaProjection捕获屏幕

    • 使用MediaCodec进行H.264编码

    • 通过Socket传输编码数据

    • 接收端解码并在SurfaceView渲染

这个完整示例涵盖了Android SurfaceView投屏的核心实现,您可以根据实际需求调整分辨率、编码参数和网络传输协议。

相关推荐
Kapaseker2 小时前
我再也不用求设计做阴影了 — Compose 阴影
android·kotlin
Digitally2 小时前
6 种简单方法:在 Mac 电脑与安卓手机之间传输文件
android
鹏程十八少2 小时前
3. 2026金三银四 Android 背完这 23 道题,Android 线程面试横着走
android·面试·前端框架
冬奇Lab13 小时前
Android 开发要变天了:Google 专为 Agent 重建工具链,Token 减少 70%、速度提升 3 倍
android·人工智能·ai编程
imuliuliang15 小时前
存储过程(SQL)
android·数据库·sql
AgCl2317 小时前
MYSQL-6-函数与约束-3/17
android·数据库·mysql
zzb158018 小时前
Fragment 生命周期深度图解:从 onAttach 到 onDetach 完整流程(面试必备)
android·java·面试·安卓
众少成多积小致巨18 小时前
Android 源码查看笔记
android·源码
angerdream18 小时前
Android手把手编写儿童手机远程监控App之前台服务
android