diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 80e372e..dfd180e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -43,8 +43,6 @@
android:name=".service.ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
-
-
remoteIceCandidates = new ArrayList<>();
-
- private WebSocketManager webSocketManager;
+ private SurfaceViewRenderer localRender;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webrtc_screen_capture);
- // 初始化控件
tvStatus = findViewById(R.id.tv_status);
btConnect = findViewById(R.id.bt_connect);
btnStartPush = findViewById(R.id.btn_start_push);
localRender = findViewById(R.id.local_render);
- textureView = findViewById(R.id.textureView);
- btConnect.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- initWebSocket();
- }
- });
+ mWebSocketManager = WebSocketManager.getInstance();
+ mWebSocketManager.addCallback(this);
- textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
- @Override
- public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
- Log.e(TAG, "onSurfaceTextureAvailable: ");
- // SurfaceTexture准备就绪,开始屏幕捕捉
- setupVirtualDisplay();
- }
+ if (mWebSocketManager.isConnected()) {
+ btConnect.setEnabled(false);
+ } else {
+ btConnect.setEnabled(true);
+ }
- @Override
- public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
- Log.e(TAG, "onSurfaceTextureSizeChanged: ");
- }
+ // 使用 Service 中统一的 EglBase 保证 Context 兼容性
+ if (ScreenCaptureService2.rootEglBase == null) {
+ ScreenCaptureService2.rootEglBase = EglBase.create();
+ }
+ localRender.init(ScreenCaptureService2.rootEglBase.getEglBaseContext(), null);
+ localRender.setMirror(false); // 屏幕采集通常不需要镜像
+ localRender.setEnableHardwareScaler(true);
- @Override
- public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
- Log.e(TAG, "onSurfaceTextureDestroyed: ");
- return false;
- }
-
- @Override
- public void onSurfaceTextureUpdated(SurfaceTexture surface) {
-// Log.e(TAG, "onSurfaceTextureUpdated: ");
- }
- });
-
- // 初始化屏幕参数
- screenWidth = getResources().getDisplayMetrics().widthPixels;
- screenHeight = getResources().getDisplayMetrics().heightPixels;
- screenDpi = getResources().getDisplayMetrics().densityDpi;
-
- // 初始化MediaProjectionManager
- mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
-
- // 检查权限
+ btConnect.setOnClickListener(v -> reconnectWebSocket());
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
- // 初始化WebRTC EGL环境
- rootEglBase = EglBase.create();
- localRender.init(rootEglBase.getEglBaseContext(), null);
- localRender.setMirror(true);
- localRender.setEnableHardwareScaler(true);
+ mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+
+ setupServiceListener();
+
+ Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent);
+ } else {
+ startService(serviceIntent);
+ }
+
+ registerReceiver(mBroadcastReceiver, new IntentFilter(ScreenCaptureService2.ACTION_PERMISSION_ACTION));
}
- private static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
+ private void setupServiceListener() {
+ ScreenCaptureService2.setStatusListener(new ScreenCaptureService2.StatusListener() {
+ @Override
+ public void onStatusChanged(String status) {
+ runOnUiThread(() -> tvStatus.setText(status));
+ }
+
+ @Override
+ public void onVideoTrack(VideoTrack track) {
+ runOnUiThread(() -> track.addSink(localRender));
+ }
+ });
+ }
+
+ private void reconnectWebSocket() {
+ mWebSocketManager.connect();
+ }
+
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.e(TAG, "onReceive: " + intent.getAction());
+ checkPermissionsAndStart();
+ }
+ };
- /**
- * 检查权限并启动采集+推流
- */
private void checkPermissionsAndStart() {
- List neededPermissions = new ArrayList<>();
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(Manifest.permission.INTERNET);
- }
-// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
-// ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
-// neededPermissions.add(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION);
-// }
-
- if (Build.VERSION.SDK_INT >= 33 &&
- ContextCompat.checkSelfPermission(this, FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(FOREGROUND_SERVICE_MEDIA_PROJECTION);
- }
-
- if (!neededPermissions.isEmpty()) {
- ActivityCompat.requestPermissions(this, neededPermissions.toArray(new String[0]), REQUEST_CODE_PERMISSIONS);
+ String permission = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
+ if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, new String[]{permission}, REQUEST_CODE_PERMISSIONS);
} else {
- // 启动前台服务
- startForegroundService();
- // 权限已满足,请求屏幕录制授权
startScreenCaptureAuthorization();
}
}
- /**
- * 发起屏幕录制授权请求
- */
private void startScreenCaptureAuthorization() {
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
}
- private void initWebSocket() {
- Log.e(TAG, "initWebSocket: ");
- // 初始化 WebSocket 管理器
- webSocketManager = WebSocketManager.getInstance();
-// String wsUrl = "ws://175.178.213.60:2310/signaling/981964879";
- String wsUrl = "ws://192.168.100.111:2310/signaling/981964879";
- webSocketManager.init(wsUrl, this);
- }
-
- /**
- * 初始化WebRTC核心组件
- */
- private void initWebRTC() {
- // 1. 初始化PeerConnectionFactory
- PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(this)
- .setEnableInternalTracer(true)
- .setFieldTrials("WebRTC-H264HighProfile/Enabled/") // 可选:启用H264
- .createInitializationOptions();
- PeerConnectionFactory.initialize(initOptions);
-
- // 2. 创建PeerConnectionFactory实例
- PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
- peerConnectionFactory = PeerConnectionFactory.builder()
- .setOptions(factoryOptions)
- .setVideoEncoderFactory(new DefaultVideoEncoderFactory(
- EglBase.create().getEglBaseContext(), true, true))
- .setVideoDecoderFactory(new DefaultVideoDecoderFactory(
- EglBase.create().getEglBaseContext()))
- .createPeerConnectionFactory();
-
- // 3. 创建自定义屏幕采集器
- screenVideoCapturer = new ScreenVideoCapturer();
-
- // 4. 创建VideoSource和VideoTrack
- SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("ScreenCaptureThread", rootEglBase.getEglBaseContext());
- videoSource = peerConnectionFactory.createVideoSource(screenVideoCapturer.isScreencast());
- screenVideoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
- screenVideoCapturer.startCapture(screenWidth, screenHeight, 30); // 30fps
-
- // 5. 创建视频轨道并绑定到本地渲染
- videoTrack = peerConnectionFactory.createVideoTrack("screen_video_track", videoSource);
- videoTrack.addSink(localRender);
-
- // 6. 创建PeerConnection(需替换为实际的ICE服务器配置)
- List iceServers = getIceServers();
-
- PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
- // 配置 ICE 传输策略(可选,根据网络环境调整)
- rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // 通常禁用 TCP 候选,使用 UDP
- rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; // 推荐启用 BUNDLE 以节省资源
- rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; // 持续收集 ICE 候选
- peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
- @Override
- public void onSignalingChange(PeerConnection.SignalingState signalingState) {
- Log.d(TAG, "PeerConnection onSignalingChange: " + signalingState);
- }
-
- @Override
- public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
- Log.d(TAG, "PeerConnection onIceConnectionChange: " + iceConnectionState);
- if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
- runOnUiThread(() -> Toast.makeText(WebRTCScreenCaptureActivity.this, "WebRTC连接成功", Toast.LENGTH_SHORT).show());
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
+ if (resultCode == RESULT_OK && data != null) {
+ Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
+ serviceIntent.setAction(ScreenCaptureService2.ACTION_START_CAPTURE);
+ serviceIntent.putExtra("resultCode", resultCode);
+ serviceIntent.putExtra("data", data);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent);
+ } else {
+ startService(serviceIntent);
}
+ btnStartPush.setText("正在推流");
+ btnStartPush.setEnabled(false);
+ Toast.makeText(this, "推流服务已启动", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, "屏幕录制授权被拒绝", Toast.LENGTH_SHORT).show();
}
-
- @Override
- public void onIceConnectionReceivingChange(boolean b) {
- Log.d(TAG, "PeerConnection onIceConnectionReceivingChange: " + b);
- }
-
- @Override
- public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
- Log.d(TAG, "PeerConnection onIceGatheringChange: " + iceGatheringState);
- // ICE候选收集完成后,可将本地SDP发送给远端
- if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
- Log.d(TAG, "ICE候选收集完成,可发送本地SDP给远端");
- }
- }
-
- @Override
- public void onIceCandidate(IceCandidate iceCandidate) {
- Log.d(TAG, "PeerConnection onIceCandidate: " + iceCandidate);
- // 将ICE候选发送给远端(实际需通过信令服务器)
- sendIceCandidateToRemote(iceCandidate);
- }
-
- @Override
- public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
- Log.d(TAG, "PeerConnection onIceCandidatesRemoved");
- }
-
- @Override
- public void onAddStream(MediaStream mediaStream) {
- Log.d(TAG, "PeerConnection onAddStream: 收到远端流");
- }
-
- @Override
- public void onRemoveStream(MediaStream mediaStream) {
- Log.d(TAG, "PeerConnection onRemoveStream");
- }
-
- @Override
- public void onDataChannel(org.webrtc.DataChannel dataChannel) {
- Log.d(TAG, "PeerConnection onDataChannel");
- }
-
- @Override
- public void onRenegotiationNeeded() {
- Log.d(TAG, "PeerConnection onRenegotiationNeeded");
- }
-
- @Override
- public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
- Log.d(TAG, "PeerConnection onAddTrack");
- }
- });
-
- // 7. 创建MediaStream并添加视频轨道
- MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream("screen_stream");
- mediaStream.addTrack(videoTrack);
- peerConnection.addStream(mediaStream);
-
- // 8. 创建Offer(发起端)
- createOffer();
- }
-
- /**
- * 创建WebRTC Offer
- */
- private void createOffer() {
- MediaConstraints constraints = new MediaConstraints();
- constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")); // 只推流,不接收视频
- constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")); // 不接收音频
-
- peerConnection.createOffer(new SdpObserver() {
- @Override
- public void onCreateSuccess(SessionDescription sessionDescription) {
- // 设置本地SDP
- peerConnection.setLocalDescription(new SdpObserver() {
- @Override
- public void onCreateSuccess(SessionDescription sessionDescription) {
- }
-
- @Override
- public void onSetSuccess() {
- // 本地SDP设置成功,通过信令服务器发送给远端
- Log.d(TAG, "本地Offer创建成功: " + sessionDescription.description);
- sendSdpToRemote(sessionDescription);
- // 将本地SDP发送给远端(实际需通过信令服务器)
- remoteSdp = sessionDescription.description; // 示例:实际需发送到远端
- }
-
- @Override
- public void onCreateFailure(String s) {
- Log.e(TAG, "创建Offer失败: " + s);
- }
-
- @Override
- public void onSetFailure(String s) {
- Log.e(TAG, "设置本地SDP失败: " + s);
- }
- }, sessionDescription);
- }
-
- @Override
- public void onSetSuccess() {
- }
-
- @Override
- public void onCreateFailure(String s) {
- Log.e(TAG, "创建Offer失败: " + s);
- }
-
- @Override
- public void onSetFailure(String s) {
- }
- }, constraints);
- }
-
- private List getIceServers() {
- List iceServers = new ArrayList<>();
-
- // 添加用户指定的 STUN 服务器
- PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478")
- .setUsername("tt")
- .setPassword("tongtong")
- .createIceServer();
- iceServers.add(stunServer);
-
- // 强烈建议添加备用的公共 STUN 服务器
- PeerConnection.IceServer publicStunServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
- .createIceServer();
- iceServers.add(publicStunServer);
-
- // 如果您的部署包含 TURN 服务器(用于可靠的中继转发),也应在此添加
- // PeerConnection.IceServer turnServer = PeerConnection.IceServer.builder("turn:your_turn_server:3478")
- // .setUsername("your_username")
- // .setPassword("your_password")
- // .createIceServer();
- // iceServers.add(turnServer);
-
- return iceServers;
- }
-
- /**
- * 发送SDP给远端信令服务器
- */
- private void sendSdpToRemote(SessionDescription sdp) {
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 1);
-
- JsonObject sdpJson = new JsonObject();
- sdpJson.addProperty("type", sdp.type.canonicalForm()); // "offer" 或 "answer"
- sdpJson.addProperty("sdp", sdp.description);
- // TODO: 2026/3/11 之后改为唯一标识码
- sdpJson.addProperty("fromUser", "981964879"); // 标识发送方
- sdpJson.addProperty("timestamp", System.currentTimeMillis());
-
- jsonObject.add("content", sdpJson);
- String json = jsonObject.toString();
-
- Log.d(TAG, "发送SDP到信令服务器: ");
- Log.e(TAG, "sendSdpToRemote: SDP内容: " + json);
-
- webSocketManager.sendMessage(json);
- }
-
- /**
- * 发送ICE Candidate给远端信令服务器
- */
- private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
- Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp);
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 2);
-
- JsonObject iceJson = new JsonObject();
- iceJson.addProperty("sdpMid", iceCandidate.sdpMid);
- iceJson.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
- iceJson.addProperty("sdp", iceCandidate.sdp);
-
- // TODO: 2026/3/11 之后改为唯一标识码
- iceJson.addProperty("fromUser", "981964879");
- iceJson.addProperty("timestamp", System.currentTimeMillis());
-
- jsonObject.add("content", iceJson);
-
- String json = jsonObject.toString();
-
- Log.d(TAG, "发送ICE Candidate到信令服务器: ");
- Log.e(TAG, "sendIceCandidateToRemote: ICE内容: " + json);
- webSocketManager.sendMessage(json);
- }
-
- /**
- * 处理信令服务器响应(接收远端的Answer SDP)
- */
- private void handleSignalingResponse(String response) {
- JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject();
- ;
-
- // 检查是否包含远端的Answer SDP
- if (jsonResponse.has("type") && "answer".equals(jsonResponse.get("type").getAsString())) {
- String remoteSdp = jsonResponse.get("sdp").getAsString();
- SessionDescription remoteSessionDescription =
- new SessionDescription(SessionDescription.Type.ANSWER, remoteSdp);
-
- // 设置远端SDP
- peerConnection.setRemoteDescription(new SimpleSdpObserver(), remoteSessionDescription);
- Log.d(TAG, "已设置远端Answer SDP");
}
-
- // 检查是否包含远端的ICE Candidate
- if (jsonResponse.has("candidate")) {
- String sdpMid = jsonResponse.get("sdpMid").getAsString();
- int sdpMLineIndex = jsonResponse.get("sdpMLineIndex").getAsInt();
- String candidate = jsonResponse.get("candidate").getAsString();
-
- IceCandidate remoteIceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate);
- peerConnection.addIceCandidate(remoteIceCandidate);
- Log.d(TAG, "已添加远端ICE Candidate");
- }
-
}
@Override
- public void onConnected() {
- Log.e(TAG, "onConnected: ");
- tvStatus.setText("websocket已连接");
- btConnect.setEnabled(false);
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == REQUEST_CODE_PERMISSIONS && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startScreenCaptureAuthorization();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ ScreenCaptureService2.setStatusListener(null);
+ if (localRender != null) {
+ localRender.release();
+ }
+ mWebSocketManager.removeCallback(this);
+ if (mBroadcastReceiver != null) {
+ unregisterReceiver(mBroadcastReceiver);
+ }
+ }
+
+ @Override
+ public void onConnected(String sessionId) {
+ Log.e(TAG, "onConnected: " + sessionId);
+ tvStatus.setText("WebSocket已连接");
}
@Override
public void onDisconnected(String reason) {
Log.e(TAG, "onDisconnected: " + reason);
- tvStatus.setText("websocket已断开:" + reason);
- btConnect.setEnabled(true);
+ tvStatus.setText("WebSocket已断开");
}
@Override
@@ -498,278 +189,12 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity implements We
@Override
public void onMessage(ByteString bytes) {
- Log.e(TAG, "onMessage: ");
+ Log.e(TAG, "onMessage: " + bytes);
}
@Override
public void onError(String error) {
Log.e(TAG, "onError: " + error);
- tvStatus.setText("websocket错误:" + error);
- btConnect.setEnabled(true);
+ tvStatus.setText("WebSocket错误:" + error);
}
-
- /**
- * 简化的SdpObserver实现
- */
- private static class SimpleSdpObserver implements SdpObserver {
- @Override
- public void onCreateSuccess(SessionDescription sessionDescription) {
- }
-
- @Override
- public void onSetSuccess() {
- Log.d(TAG, "SDP设置成功");
- }
-
- @Override
- public void onCreateFailure(String error) {
- Log.e(TAG, "创建SDP失败: " + error);
- }
-
- @Override
- public void onSetFailure(String error) {
- Log.e(TAG, "设置SDP失败: " + error);
- }
- }
-
- /**
- * 自定义屏幕视频采集器(核心:对接MediaProjection和WebRTC)
- */
- private class ScreenVideoCapturer implements VideoCapturer {
- private CapturerObserver capturerObserver;
- private SurfaceTextureHelper surfaceTextureHelper;
- private Surface surface;
-
- @Override
- public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
- this.surfaceTextureHelper = surfaceTextureHelper;
- this.capturerObserver = capturerObserver;
- this.surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
-
- // 创建虚拟显示器,将屏幕内容输出到WebRTC的Surface
- createVirtualDisplay();
- }
-
- @Override
- public void startCapture(int width, int height, int framerate) {
- capturerObserver.onCapturerStarted(true);
- Log.d(TAG, "Screen capturer started: " + width + "x" + height + " " + framerate + "fps");
- }
-
- @Override
- public void stopCapture() throws InterruptedException {
- capturerObserver.onCapturerStopped();
- releaseVirtualDisplay();
- }
-
- @Override
- public void changeCaptureFormat(int width, int height, int framerate) {
- // 适配分辨率变化
- releaseVirtualDisplay();
- screenWidth = width;
- screenHeight = height;
- createVirtualDisplay();
- }
-
- @Override
- public void dispose() {
- surfaceTextureHelper.dispose();
- releaseVirtualDisplay();
- stopForegroundService();
- }
-
- @Override
- public boolean isScreencast() {
- return true; // 标记为屏幕采集(非摄像头)
- }
-
- private void createVirtualDisplay() {
- if (mediaProjection == null || surface == null) {
- return;
- }
- virtualDisplay = mediaProjection.createVirtualDisplay(
- "WebRTC-ScreenCapture",
- screenWidth,
- screenHeight,
- screenDpi,
- DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
- surface,
- null,
- null
- );
- }
-
- private void releaseVirtualDisplay() {
- if (virtualDisplay != null) {
- virtualDisplay.release();
- virtualDisplay = null;
- }
- }
- }
-
- /**
- * 启动前台服务(Android 10+ 必须)
- */
- private void startForegroundService() {
- Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForegroundService(serviceIntent);
- } else {
- startService(serviceIntent);
- }
- }
-
- /**
- * 停止前台服务
- */
- private void stopForegroundService() {
- stopService(new Intent(this, ScreenCaptureService2.class));
- }
-
- /**
- * 释放虚拟显示器
- */
- private void releaseVirtualDisplay() {
- if (virtualDisplay != null) {
- virtualDisplay.release();
- virtualDisplay = null;
- }
- }
-
- /**
- * 处理权限请求结果
- */
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == REQUEST_CODE_PERMISSIONS) {
- boolean allGranted = true;
- for (int result : grantResults) {
- if (result != PackageManager.PERMISSION_GRANTED) {
- allGranted = false;
- break;
- }
- }
- if (allGranted) {
- startScreenCaptureAuthorization();
- } else {
- Toast.makeText(this, "权限申请失败,无法启动推流", Toast.LENGTH_SHORT).show();
- }
- }
- }
-
- /**
- * 处理屏幕录制授权结果
- */
- @Override
- protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
- if (resultCode == RESULT_OK && data != null) {
-
- // 授权成功,获取MediaProjection
- mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
- mediaProjection.registerCallback(new MediaProjection.Callback() {
- @Override
- public void onStop() {
- super.onStop();
- Toast.makeText(WebRTCScreenCaptureActivity.this, "屏幕采集已停止", Toast.LENGTH_SHORT).show();
- releaseResources();
- }
- }, null);
-
- setupVirtualDisplay();
-
- // 初始化WebRTC并开始推流
- initWebRTC();
-
- // 更新按钮状态
- btnStartPush.setText("停止推流");
- btnStartPush.setOnClickListener(v -> releaseResources());
- } else {
- Toast.makeText(this, "屏幕录制授权被拒绝", Toast.LENGTH_SHORT).show();
- }
- }
- }
-
- private void setupVirtualDisplay() {
- if (mediaProjection == null) return;
- // 获取屏幕尺寸和密度
- Log.e(TAG, "setupVirtualDisplay: ");
-
- // 从TextureView的SurfaceTexture创建Surface
- Surface surface = new Surface(textureView.getSurfaceTexture());
- // 创建虚拟显示器,将屏幕内容投射到Surface
- mVirtualDisplay = mediaProjection.createVirtualDisplay(
- "ScreenCapture",
- screenWidth, screenHeight, screenDpi,
- DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
- surface, null, null);
- }
-
- /**
- * 释放所有资源
- */
- private void releaseResources() {
- // 停止WebRTC
- if (peerConnection != null) {
- peerConnection.close();
- peerConnection = null;
- }
- if (videoTrack != null) {
- videoTrack.dispose();
- videoTrack = null;
- }
- if (videoSource != null) {
- videoSource.dispose();
- videoSource = null;
- }
- if (peerConnectionFactory != null) {
- peerConnectionFactory.dispose();
- peerConnectionFactory = null;
- }
-
- // 停止屏幕采集
- if (screenVideoCapturer != null) {
- try {
- screenVideoCapturer.stopCapture();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- screenVideoCapturer.dispose();
- screenVideoCapturer = null;
- }
-
- // 释放MediaProjection
- if (mediaProjection != null) {
- mediaProjection.stop();
- mediaProjection = null;
- }
-
- // 停止前台服务
- stopForegroundService();
-
- // 释放渲染
- if (localRender != null) {
- localRender.release();
- }
-// if (rootEglBase != null) {
-// rootEglBase.release();
-// }
-
- if (webSocketManager != null) {
- webSocketManager.disconnect();
- }
-
- // 重置按钮
- btnStartPush.setText("开始推流");
- btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- releaseResources();
- }
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java
index 8633492..7528bb1 100644
--- a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java
+++ b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java
@@ -1,21 +1,15 @@
package com.ttstd.remoteservice.activity.main;
-import android.Manifest;
import android.content.Intent;
-import android.content.pm.PackageManager;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.util.Log;
import android.view.View;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
import com.ttstd.remoteservice.R;
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity;
import com.ttstd.remoteservice.databinding.ActivityMainBinding;
-import com.ttstd.remoteservice.service.ControlService;
import com.ttstd.remoteservice.service.ScreenCaptureService;
public class MainActivity extends BaseMvvmActivity {
@@ -67,10 +61,6 @@ public class MainActivity extends BaseMvvmActivity mCallbackSet = new CopyOnWriteArraySet<>();
// 连接状态
- private boolean isConnected = false;
+ private boolean mConnected = false;
private boolean shouldReconnect = true;
private Handler mainHandler = new Handler(Looper.getMainLooper());
@@ -49,61 +58,66 @@ public class WebSocketManager {
// 连接状态锁
private final Object connectionLock = new Object();
- private WebSocketManager() {
- // 私有构造函数
+ private final String mLocalUserId;
+ private final WebSocketListener listener = new InnerWebSocketListener();
+
+ private WebSocketManager(Context context) {
+ this.mContext = context.getApplicationContext();
+ this.mLocalUserId = SystemUtils.getSerial();
+
+ // 创建OkHttpClient,支持WebSocket
+ client = new OkHttpClient.Builder()
+ .pingInterval(WebSocketConfig.pingInterval, TimeUnit.SECONDS) // 保持连接活跃
+ .connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
+ .readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
+ .writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
+ .build();
+ request = new Request.Builder()
+ .url(WEBSOCKET_URL + mLocalUserId)
+ .build();
+
+ webSocket = client.newWebSocket(request, listener);
}
- public static WebSocketManager getInstance() {
- if (instance == null) {
- synchronized (WebSocketManager.class) {
- if (instance == null) {
- instance = new WebSocketManager();
- }
- }
- }
- return instance;
- }
/**
* 初始化 WebSocket 连接
*/
- public void init(String url, WebSocketCallback callback) {
- this.wsUrl = url;
- this.callback = callback;
+ public static void init(Context context) {
+ if (instance == null) {
+ synchronized (WebSocketManager.class) {
+ if (instance == null) {
+ instance = new WebSocketManager(context);
+ }
+ }
+ }
+ }
- client = new OkHttpClient.Builder()
- .connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
- .readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
- .writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
- .pingInterval(10, TimeUnit.SECONDS) // 设置心跳
- .retryOnConnectionFailure(true)
- .build();
+ public static WebSocketManager getInstance() {
+ if (instance == null) {
+ throw new IllegalStateException("WebSocketManager must be initialized first with init(Context)");
+ }
+ return instance;
+ }
- connect();
+ public void addCallback(WebSocketCallback listener) {
+ this.mCallbackSet.add(listener);
+ if (isConnected()) {
+ listener.onConnected("连接成功");
+ }
+ }
+
+ public void removeCallback(WebSocketCallback listener) {
+ this.mCallbackSet.remove(listener);
}
/**
* 建立 WebSocket 连接
*/
- private void connect() {
- if (wsUrl == null) {
- Log.e(TAG, "WebSocket URL 不能为 null");
- return;
- }
-
- if (TextUtils.isEmpty(wsUrl)) {
- Log.e(TAG, "WebSocketConfig or URL is null");
- return;
- }
-
+ public void connect() {
try {
- Request request = new Request.Builder()
- .url(wsUrl)
- .build();
-
- webSocket = client.newWebSocket(request, new InnerWebSocketListener());
- Log.d(TAG, "Connecting to WebSocket: " + wsUrl);
-
+ if (mConnected && webSocket != null) return;
+ webSocket = client.newWebSocket(request, listener);
} catch (Exception e) {
Log.e(TAG, "Connect failed: " + e.getMessage());
notifyError("连接失败: " + e.getMessage());
@@ -114,7 +128,7 @@ public class WebSocketManager {
* 发送文本消息
*/
public boolean sendMessage(String message) {
- if (webSocket != null && isConnected) {
+ if (webSocket != null && mConnected) {
boolean sent = webSocket.send(message);
if (sent) {
Log.d(TAG, "Message sent: " + message);
@@ -137,7 +151,7 @@ public class WebSocketManager {
* 发送二进制消息
*/
public boolean sendMessage(ByteString bytes) {
- if (webSocket != null && isConnected) {
+ if (webSocket != null && mConnected) {
return webSocket.send(bytes);
} else {
Log.e(TAG, "WebSocket not connected, cannot send binary message");
@@ -162,7 +176,7 @@ public class WebSocketManager {
webSocket = null;
}
- isConnected = false;
+ mConnected = false;
Log.d(TAG, "WebSocket 连接已关闭");
reconnectAttempts = 0;
}
@@ -171,7 +185,7 @@ public class WebSocketManager {
* 获取连接状态
*/
public boolean isConnected() {
- return isConnected;
+ return mConnected && webSocket != null;
}
/**
@@ -183,10 +197,10 @@ public class WebSocketManager {
heartbeatRunnable = new Runnable() {
@Override
public void run() {
- if (isConnected && webSocket != null) {
+ if (mConnected && webSocket != null) {
JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 0);
- jsonObject.addProperty("content", "ping");
+ jsonObject.addProperty("type", "ping");
+ jsonObject.addProperty("target", "ping");
// 发送心跳消息(可以是空消息或特定协议)
boolean sent = webSocket.send(jsonObject.toString());
Log.d(TAG, "发送心跳包");
@@ -210,7 +224,7 @@ public class WebSocketManager {
private void handleDisconnect(String reason) {
Log.d(TAG, "Handle disconnect: " + reason);
- isConnected = false;
+ mConnected = false;
stopHeartbeat();
notifyDisconnected(reason);
@@ -270,7 +284,7 @@ public class WebSocketManager {
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
Log.d(TAG, "WebSocket connected successfully");
- isConnected = true;
+ mConnected = true;
reconnectAttempts = 0; // 重置重连计数器
// 连接成功,发送队列中的积压消息
@@ -278,8 +292,8 @@ public class WebSocketManager {
// 在主线程通知连接成功
mainHandler.post(() -> {
- if (callback != null) {
- callback.onConnected();
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onConnected("连接成功");
}
});
@@ -293,7 +307,7 @@ public class WebSocketManager {
// 在主线程通知消息接收
mainHandler.post(() -> {
- if (callback != null) {
+ for (WebSocketCallback callback : mCallbackSet) {
callback.onMessage(text);
}
});
@@ -305,7 +319,7 @@ public class WebSocketManager {
// 在主线程通知消息接收
mainHandler.post(() -> {
- if (callback != null) {
+ for (WebSocketCallback callback : mCallbackSet) {
callback.onMessage(bytes);
}
});
@@ -338,7 +352,7 @@ public class WebSocketManager {
synchronized (connectionLock) {
while (!messageQueue.isEmpty()) {
String message = messageQueue.poll();
- if (message != null && isConnected && webSocket != null) {
+ if (message != null && mConnected && webSocket != null) {
boolean sent = webSocket.send(message);
if (sent) {
Log.d(TAG, "队列消息发送成功: " + message);
@@ -382,8 +396,8 @@ public class WebSocketManager {
*/
private void notifyError(String error) {
mainHandler.post(() -> {
- if (callback != null) {
- callback.onError(error);
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onError("检查网络");
}
});
}
@@ -393,8 +407,8 @@ public class WebSocketManager {
*/
private void notifyDisconnected(String reason) {
mainHandler.post(() -> {
- if (callback != null) {
- callback.onDisconnected(reason);
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onDisconnected("连接关闭中: " + reason);
}
});
}
diff --git a/app/src/main/java/com/ttstd/remoteservice/service/ControlService.java b/app/src/main/java/com/ttstd/remoteservice/service/ControlService.java
deleted file mode 100644
index 1836778..0000000
--- a/app/src/main/java/com/ttstd/remoteservice/service/ControlService.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.ttstd.remoteservice.service;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import androidx.annotation.Nullable;
-
-public class ControlService extends Service {
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- return super.onStartCommand(intent, flags, startId);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- }
-
- @Override
- public void onLowMemory() {
- super.onLowMemory();
- }
-
- @Override
- public void onTrimMemory(int level) {
- super.onTrimMemory(level);
- }
-
-}
diff --git a/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java
index ef354e8..a49543b 100644
--- a/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java
+++ b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java
@@ -1,36 +1,681 @@
package com.ttstd.remoteservice.service;
+import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.projection.MediaProjection;
+import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
+import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.Surface;
+import android.view.WindowManager;
+import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
-public class ScreenCaptureService2 extends Service {
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
+import com.ttstd.remoteservice.bean.SignalingMessage;
+import com.ttstd.remoteservice.network.WebSocketCallback;
+import com.ttstd.remoteservice.network.WebSocketManager;
+import com.ttstd.remoteservice.utils.SystemUtils;
+
+import org.webrtc.CapturerObserver;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoSource;
+import org.webrtc.VideoTrack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import okio.ByteString;
+
+public class ScreenCaptureService2 extends Service implements WebSocketCallback {
private static final String TAG = "ScreenCaptureService2";
private static final String CHANNEL_ID = "ScreenCaptureChannel";
private static final int NOTIFICATION_ID = 1001;
+ public static final String ACTION_START_CAPTURE = "com.ttstd.remoteservice.ACTION_START_CAPTURE";
+ public static final String ACTION_PERMISSION_ACTION = "com.ttstd.remoteservice.ACTION_PERMISSION";
+
+ private final Gson mGson = new Gson();
+
+ // WebRTC核心组件
+ public static EglBase rootEglBase;
+ private PeerConnectionFactory peerConnectionFactory;
+ private PeerConnection peerConnection;
+ private VideoSource videoSource;
+ private static VideoTrack videoTrack;
+ private MediaProjectionManager mediaProjectionManager;
+ private MediaProjection mediaProjection;
+ private VirtualDisplay virtualDisplay;
+ private ScreenVideoCapturer screenVideoCapturer;
+
+ private static StatusListener statusListener;
+
+ public interface StatusListener {
+ void onStatusChanged(String status);
+
+ void onVideoTrack(VideoTrack track);
+ }
+
+ public static void setStatusListener(StatusListener listener) {
+ statusListener = listener;
+ if (statusListener != null && videoTrack != null) {
+ statusListener.onVideoTrack(videoTrack);
+ }
+ }
+
+ private int screenWidth;
+ private int screenHeight;
+ private int screenDpi;
+
+ private WebSocketManager webSocketManager;
+ private String mRemoteUserId;
+
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate: ");
createNotificationChannel();
if (Build.VERSION.SDK_INT >= 31) {
- // Android 12+ 必须指定前台服务类型
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else {
- // 低版本直接启动
startForeground(NOTIFICATION_ID, createNotification());
}
+
+ webSocketManager = WebSocketManager.getInstance();
+ webSocketManager.addCallback(this);
+ initScreenParams();
+ }
+
+ private void initScreenParams() {
+ WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
+ screenWidth = metrics.widthPixels;
+ screenHeight = metrics.heightPixels;
+ screenDpi = metrics.densityDpi;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.e(TAG, "onStartCommand: ");
+ if (intent != null) {
+ String action = intent.getAction();
+
+ if (ACTION_START_CAPTURE.equals(action)) {
+ int resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED);
+ Intent data = intent.getParcelableExtra("data");
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ startScreenCapture(resultCode, data);
+ }
+ } else {
+ // 兼容旧逻辑,如果没有 Action 则根据数据判断
+ int resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED);
+ Intent data = intent.getParcelableExtra("data");
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ startScreenCapture(resultCode, data);
+ }
+ }
+ }
+ return START_STICKY;
+ }
+
+ private void startScreenCapture(int resultCode, Intent data) {
+ mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+ mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
+
+ mediaProjection.registerCallback(new MediaProjection.Callback() {
+ @Override
+ public void onStop() {
+ super.onStop();
+ releaseResources();
+ }
+ }, null);
+
+ initWebRTC();
+ }
+
+ private void initWebRTC() {
+ Log.e(TAG, "Initializing WebRTC in Service");
+
+ // 1. 初始化PeerConnectionFactory
+ PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(this)
+ .setEnableInternalTracer(true)
+ .setFieldTrials("WebRTC-H264HighProfile/Enabled/")
+ .createInitializationOptions();
+ PeerConnectionFactory.initialize(initOptions);
+
+ if (rootEglBase == null) {
+ rootEglBase = EglBase.create();
+ }
+
+ // 2. 创建PeerConnectionFactory实例
+ peerConnectionFactory = PeerConnectionFactory.builder()
+ .setVideoEncoderFactory(new DefaultVideoEncoderFactory(
+ rootEglBase.getEglBaseContext(), true, true))
+ .setVideoDecoderFactory(new DefaultVideoDecoderFactory(
+ rootEglBase.getEglBaseContext()))
+ .createPeerConnectionFactory();
+
+ // 3. 创建自定义屏幕采集器
+ screenVideoCapturer = new ScreenVideoCapturer();
+
+ // 4. 创建VideoSource和VideoTrack
+ SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("ScreenCaptureThread", rootEglBase.getEglBaseContext());
+ videoSource = peerConnectionFactory.createVideoSource(screenVideoCapturer.isScreencast());
+ screenVideoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
+ screenVideoCapturer.startCapture(screenWidth, screenHeight, 30);
+
+ videoTrack = peerConnectionFactory.createVideoTrack("screen_video_track", videoSource);
+
+ if (statusListener != null) {
+ statusListener.onVideoTrack(videoTrack);
+ }
+
+ // 5. 创建PeerConnection
+ List iceServers = getIceServers();
+ PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
+ rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
+ rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
+ rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
+
+ peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.e(TAG, "initWebRTC onSignalingChange: ");
+ }
+
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
+ Log.e(TAG, "initWebRTC onIceConnectionChange: " + iceConnectionState);
+ }
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {
+ Log.e(TAG, "initWebRTC onIceConnectionReceivingChange: " + b);
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
+ Log.e(TAG, "initWebRTC onIceGatheringChange: ");
+ }
+
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ Log.e(TAG, "initWebRTC onIceCandidate: ");
+ sendIceCandidateToRemote(iceCandidate);
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
+ Log.e(TAG, "initWebRTC onIceCandidatesRemoved: ");
+ }
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ Log.e(TAG, "initWebRTC onAddStream: ");
+ }
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {
+ Log.e(TAG, "initWebRTC onRemoveStream: ");
+ }
+
+ @Override
+ public void onDataChannel(org.webrtc.DataChannel dataChannel) {
+ Log.e(TAG, "initWebRTC onDataChannel: ");
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.e(TAG, "initWebRTC onRenegotiationNeeded: ");
+ handleRenegotiation();
+ }
+
+ @Override
+ public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ Log.e(TAG, "initWebRTC onAddTrack: ");
+ }
+ });
+
+ // 6. 添加轨道
+ if (peerConnection != null) {
+ peerConnection.addTrack(videoTrack);
+ }
+
+// // 7. 创建Offer
+// if (peerConnection != null) {
+// createOffer();
+// }
+ }
+
+ private void handleRenegotiation() {
+ if (peerConnection == null) {
+ Log.e(TAG, "handleRenegotiation: peerConnection is null");
+ return;
+ }
+
+ Log.e(TAG, "开始重新协商,创建新的 Offer");
+
+ MediaConstraints constraints = new MediaConstraints();
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
+
+ peerConnection.createOffer(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e(TAG, "重新协商 Offer 创建成功");
+ peerConnection.setLocalDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e(TAG, "重新协商本地描述设置成功");
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "重新协商本地描述设置完成,发送 Offer 到远端");
+ sendSdpToRemote(sessionDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e(TAG, "重新协商设置本地描述失败: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "重新协商设置本地描述失败: " + s);
+ }
+ }, sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "重新协商 Offer onSetSuccess");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e(TAG, "重新协商创建 Offer 失败: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "重新协商 Offer onSetFailure: " + s);
+ }
+ }, constraints);
+ }
+
+ private List getIceServers() {
+ List iceServers = new ArrayList<>();
+ // 使用用户指定的 STUN 服务器
+ iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
+ // 备用
+ iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
+ return iceServers;
+ }
+
+ private void createOffer() {
+ MediaConstraints constraints = new MediaConstraints();
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
+
+ peerConnection.createOffer(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ peerConnection.setLocalDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e(TAG, "createOffer onCreateSuccess onCreateSuccess: " + sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "createOffer onCreateSuccess onSetSuccess: ");
+ sendSdpToRemote(sessionDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e(TAG, "createOffer onCreateSuccess onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "createOffer onCreateSuccess onSetFailure: " + s);
+ }
+ }, sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "createOffer onSetSuccess: ");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e(TAG, "createOffer onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "createOffer onSetFailure: " + s);
+ }
+ }, constraints);
+ }
+
+ private static final String MSG_TYPE_ASK = "ask";
+ private static final String MSG_TYPE_OFFER = "offer";
+ private static final String MSG_TYPE_ANSWER = "answer";
+ private static final String MSG_TYPE_SDP = "sdp";
+ private static final String MSG_TYPE_ICE = "iceCandidate";
+
+ private void sendSdpToRemote(SessionDescription description) {
+ String type = description.type.canonicalForm();
+ SignalingMessage message = new SignalingMessage(type, SystemUtils.getSerial(), mRemoteUserId, description);
+ sendSignalingMessage(type, message);
+ }
+
+ private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
+ SignalingMessage message = new SignalingMessage("iceCandidate", SystemUtils.getSerial(), mRemoteUserId, iceCandidate);
+ sendSignalingMessage(MSG_TYPE_ICE, message);
+ }
+
+ private void sendSignalingMessage(String type, SignalingMessage message) {
+ if (webSocketManager == null || !webSocketManager.isConnected()) return;
+
+ // 按照服务器要求的包装格式发送
+ JsonObject wrapper = new JsonObject();
+ wrapper.addProperty("type", type);
+ wrapper.addProperty("target", mRemoteUserId);
+ wrapper.add("data", mGson.toJsonTree(message));
+
+ webSocketManager.sendMessage(wrapper.toString());
+ }
+
+ private void handleSignalingMessage(String message) {
+ try {
+ JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject();
+ if (!jsonObject.has("type")) return;
+
+ String type = jsonObject.get("type").getAsString();
+ if (!jsonObject.has("data") || !jsonObject.get("data").isJsonObject()) {
+ Log.e(TAG, "handleSignalingMessage: 'data' is missing or not an object");
+ return;
+ }
+ JsonObject content = jsonObject.getAsJsonObject("data");
+
+ switch (type) {
+ case MSG_TYPE_ASK:
+ handleAskMessage(content);
+ break;
+ case MSG_TYPE_OFFER:
+ handleOfferMessage(content);
+ break;
+ case MSG_TYPE_ANSWER:
+ handleAnswerMessage(content);
+ break;
+ case MSG_TYPE_SDP:
+ handleSdpMessage(content);
+ break;
+ case MSG_TYPE_ICE:
+ handleIceCandidateMessage(content);
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "解析信令消息失败: " + e.getMessage(), e);
+ }
+ }
+
+ int mAnalogConnection = 1;
+
+ /**
+ * 收到连接请求
+ *
+ * @param content
+ */
+ private void handleAskMessage(JsonObject content) {
+ SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
+ if (signalingMessage != null) {
+ String senderId = signalingMessage.getSenderId();
+ mRemoteUserId = senderId;
+ Log.e(TAG, "收到来自 " + senderId + " 的 连接请求");
+ Toast.makeText(this, "收到来自 " + senderId + " 的 连接请求", Toast.LENGTH_SHORT).show();
+ // TODO: 2026/6/4 弹窗询问是否接受
+ if (mAnalogConnection % 2 == 0) {
+ sendRejectMessage(senderId);
+ } else {
+ sendAcceptMessage(senderId);
+ }
+ }
+
+ mAnalogConnection++;
+ }
+
+ private void sendRejectMessage(String senderId) {
+ Log.e(TAG, "sendRejectMessage: reject " + senderId);
+ SignalingMessage message = new SignalingMessage("reject", SystemUtils.getSerial(), senderId, null);
+ sendSignalingMessage("reject", message);
+ }
+
+ private void sendAcceptMessage(String senderId) {
+ Log.e(TAG, "sendAcceptMessage: accept " + senderId);
+ SignalingMessage message = new SignalingMessage("accept", SystemUtils.getSerial(), senderId, null);
+ sendSignalingMessage("accept", message);
+ Intent intent = new Intent(ACTION_PERMISSION_ACTION);
+// intent.setAction(ACTION_PERMISSION_ACTION);
+ sendBroadcast(intent);
+ }
+
+
+ private void handleOfferMessage(JsonObject content) {
+ SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
+ if (signalingMessage != null) {
+ mRemoteUserId = signalingMessage.getSenderId();
+ Log.e(TAG, "收到来自 " + mRemoteUserId + " 的 Offer");
+ }
+ // TODO: 2026/6/4 暂时不需要接受远程逻辑
+ // 复用 handleSdpMessage 的逻辑来设置远程描述并创建 Answer
+ handleSdpMessage(content);
+ }
+
+ private void handleAnswerMessage(JsonObject content) {
+ SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
+ if (signalingMessage != null) {
+ mRemoteUserId = signalingMessage.getSenderId();
+ Log.e(TAG, "收到来自 " + mRemoteUserId + " 的 Answer");
+ }
+ handleSdpMessage(content);
+ }
+
+
+ private void handleSdpMessage(JsonObject content) {
+ if (peerConnection == null) return;
+
+ if (!content.has("type") || !content.has("payload")) {
+ Log.e(TAG, "handleSdpMessage: 'type' or 'payload' missing");
+ return;
+ }
+
+ String type = content.get("type").getAsString();
+ if (!content.get("payload").isJsonObject()) {
+ Log.e(TAG, "handleSdpMessage: 'payload' is not an object, type=" + type);
+ return;
+ }
+ JsonObject payload = content.getAsJsonObject("payload");
+
+ if (!payload.has("description")) {
+ Log.e(TAG, "handleSdpMessage: 'payload' missing 'description'");
+ return;
+ }
+ String sdpDescription = payload.get("description").getAsString();
+
+ if ("offer".equals(type)) {
+ peerConnection.setRemoteDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "设置远端 Offer 成功,正在创建 Answer");
+ createAnswer();
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "设置远端 Offer 失败: " + s);
+ }
+ }, new SessionDescription(SessionDescription.Type.OFFER, sdpDescription));
+ } else if ("answer".equals(type)) {
+ peerConnection.setRemoteDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, "设置远端 Answer 成功");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, "设置远端 Answer 失败: " + s);
+ }
+ }, new SessionDescription(SessionDescription.Type.ANSWER, sdpDescription));
+ }
+ }
+
+ private void handleIceCandidateMessage(JsonObject content) {
+ if (peerConnection == null) return;
+
+ if (!content.has("payload") || !content.get("payload").isJsonObject()) {
+ Log.e(TAG, "handleIceCandidateMessage: 'payload' is missing or not an object");
+ return;
+ }
+ JsonObject payload = content.getAsJsonObject("payload");
+ if (!payload.has("sdpMid") || !payload.has("sdpMLineIndex") || !payload.has("sdp")) {
+ Log.e(TAG, "handleIceCandidateMessage: payload missing fields");
+ return;
+ }
+
+ String sdpMid = payload.get("sdpMid").getAsString();
+ int sdpMLineIndex = payload.get("sdpMLineIndex").getAsInt();
+ String candidate = payload.get("sdp").getAsString();
+ peerConnection.addIceCandidate(new IceCandidate(sdpMid, sdpMLineIndex, candidate));
+ }
+
+ private void createAnswer() {
+ MediaConstraints constraints = new MediaConstraints();
+ // 作为被控端,我们通常只发视频,不收视频/音频(或者根据需要配置)
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
+ constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
+
+ peerConnection.createAnswer(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ peerConnection.setLocalDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ }
+
+ @Override
+ public void onSetSuccess() {
+ sendSdpToRemote(sessionDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ }
+ }, sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ }
+ }, constraints);
+ }
+
+
+ @Override
+ public void onConnected(String sessionId) {
+ Log.e(TAG, "WebSocket connected");
+ if (statusListener != null) {
+ statusListener.onStatusChanged("WebSocket已连接");
+ }
+ }
+
+ @Override
+ public void onDisconnected(String reason) {
+ Log.e(TAG, "WebSocket disconnected: " + reason);
+ if (statusListener != null) {
+ statusListener.onStatusChanged("WebSocket已断开: " + reason);
+ }
+ }
+
+ @Override
+ public void onMessage(String message) {
+ Log.e(TAG, "onMessage: " + message);
+ handleSignalingMessage(message);
+ }
+
+ @Override
+ public void onMessage(ByteString bytes) {
+ Log.e(TAG, "onMessage ByteString: " + bytes.size());
+ }
+
+ @Override
+ public void onError(String error) {
+ Log.e(TAG, "WebSocket error: " + error);
+ if (statusListener != null) {
+ statusListener.onStatusChanged("WebSocket错误: " + error);
+ }
}
@Nullable
@@ -39,11 +684,6 @@ public class ScreenCaptureService2 extends Service {
return null;
}
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- return START_STICKY; // 服务被杀死后尝试重启
- }
-
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
@@ -64,4 +704,117 @@ public class ScreenCaptureService2 extends Service {
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
}
-}
\ No newline at end of file
+
+ private void releaseResources() {
+ if (peerConnection != null) {
+ peerConnection.close();
+ peerConnection = null;
+ }
+ if (videoTrack != null) {
+ videoTrack.dispose();
+ videoTrack = null;
+ }
+ if (videoSource != null) {
+ videoSource.dispose();
+ videoSource = null;
+ }
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ peerConnectionFactory = null;
+ }
+ if (screenVideoCapturer != null) {
+ try {
+ screenVideoCapturer.stopCapture();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ screenVideoCapturer.dispose();
+ screenVideoCapturer = null;
+ }
+ if (mediaProjection != null) {
+ mediaProjection.stop();
+ mediaProjection = null;
+ }
+ if (rootEglBase != null) {
+ rootEglBase.release();
+ rootEglBase = null;
+ }
+ webSocketManager.removeCallback(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ releaseResources();
+ }
+
+ private class ScreenVideoCapturer implements VideoCapturer {
+ private CapturerObserver capturerObserver;
+ private SurfaceTextureHelper surfaceTextureHelper;
+ private Surface surface;
+
+ @Override
+ public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
+ this.surfaceTextureHelper = surfaceTextureHelper;
+ this.capturerObserver = capturerObserver;
+
+ // 设置 SurfaceTexture 的尺寸,防止默认尺寸导致画面拉伸或无法采集
+ surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(screenWidth, screenHeight);
+ this.surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
+
+ // 监听新帧并交给 capturerObserver 处理,这是显示画面的关键
+ surfaceTextureHelper.startListening(videoFrame -> {
+ capturerObserver.onFrameCaptured(videoFrame);
+ });
+
+ createVirtualDisplay();
+ }
+
+ @Override
+ public void startCapture(int width, int height, int framerate) {
+ capturerObserver.onCapturerStarted(true);
+ }
+
+ @Override
+ public void stopCapture() throws InterruptedException {
+ capturerObserver.onCapturerStopped();
+ releaseVirtualDisplay();
+ }
+
+ @Override
+ public void changeCaptureFormat(int width, int height, int framerate) {
+ releaseVirtualDisplay();
+ createVirtualDisplay();
+ }
+
+ @Override
+ public void dispose() {
+ if (surfaceTextureHelper != null) {
+ surfaceTextureHelper.dispose();
+ }
+ releaseVirtualDisplay();
+ }
+
+ @Override
+ public boolean isScreencast() {
+ return true;
+ }
+
+ private void createVirtualDisplay() {
+ if (mediaProjection == null || surface == null) return;
+ virtualDisplay = mediaProjection.createVirtualDisplay(
+ "WebRTC-ScreenCapture",
+ screenWidth, screenHeight, screenDpi,
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+ surface, null, null
+ );
+ }
+
+ private void releaseVirtualDisplay() {
+ if (virtualDisplay != null) {
+ virtualDisplay.release();
+ virtualDisplay = null;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java b/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java
index 259c264..66aba3d 100644
--- a/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java
+++ b/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java
@@ -6,6 +6,8 @@ import android.content.Context;
import android.os.Build;
import android.util.Log;
+import com.ttstd.remoteservice.BuildConfig;
+
import java.lang.reflect.Method;
import java.util.List;
@@ -18,6 +20,9 @@ public class SystemUtils {
*/
@SuppressLint("MissingPermission")
public static String getSerial() {
+ if (BuildConfig.DEBUG) {
+ return "981964879";
+ }
String serial = "unknow";
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+