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投屏的核心实现,您可以根据实际需求调整分辨率、编码参数和网络传输协议。

相关推荐
赏金术士5 小时前
Kotlin ViewModel
android·kotlin
vistaup7 小时前
kotlin 二维码实现高斯模糊
android·kotlin
愈努力俞幸运7 小时前
function calling与mcp
android·数据库·redis
阿巴斯甜8 小时前
LeakCanary
android
阿巴斯甜9 小时前
compose
android
阿巴斯甜9 小时前
Glide
android
-SOLO-9 小时前
使用Perfetto debug trace查看超时slice
android
阿巴斯甜9 小时前
Retrofit
android
阿巴斯甜9 小时前
OkHttp
android
阿巴斯甜10 小时前
Flow
android