From 898fb69aacdb74a36981a8cd78dfb3320689cc7a Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Tue, 23 Jun 2026 10:38:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E6=8D=A2=E4=BF=A1=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 - .../activity/WebRTCScreenCaptureActivity.java | 785 +++--------------- .../activity/main/MainActivity.java | 10 - .../remoteservice/base/BaseApplication.java | 2 + .../remoteservice/bean/SignalingMessage.java | 49 ++ .../remoteservice/bean/WebRTCMessage.java | 34 + .../remoteservice/config/WebSocketConfig.java | 2 + .../network/WebSocketCallback.java | 2 +- .../network/WebSocketManager.java | 140 ++-- .../remoteservice/service/ControlService.java | 41 - .../service/ScreenCaptureService2.java | 771 ++++++++++++++++- .../remoteservice/utils/SystemUtils.java | 5 + 12 files changed, 1037 insertions(+), 806 deletions(-) create mode 100644 app/src/main/java/com/ttstd/remoteservice/bean/SignalingMessage.java create mode 100644 app/src/main/java/com/ttstd/remoteservice/bean/WebRTCMessage.java delete mode 100644 app/src/main/java/com/ttstd/remoteservice/service/ControlService.java 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+