diff --git a/app/build.gradle b/app/build.gradle index d87bc1d..ee0c636 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,6 +100,9 @@ dependencies { /*https://github.com/JeremyLiao/LiveEventBus*/ implementation 'com.jeremyliao:live-event-bus-x:1.7.3' + implementation 'org.webrtc:google-webrtc:1.0.32006' + implementation 'org.java-websocket:Java-WebSocket:1.5.3' + //MMKV implementation 'com.tencent:mmkv-static:1.2.14' //bugly diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a849a4d..80e372e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,9 +8,10 @@ - - - + + + + + + + diff --git a/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java b/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java new file mode 100644 index 0000000..8576f42 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java @@ -0,0 +1,620 @@ +package com.ttstd.remoteservice.activity; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.SurfaceTexture; +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.Bundle; +import android.util.Log; +import android.view.Surface; +import android.view.TextureView; +import android.widget.Button; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.ttstd.remoteservice.R; +import com.ttstd.remoteservice.service.ScreenCaptureService2; + +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.SurfaceViewRenderer; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; + +import java.util.ArrayList; +import java.util.List; + +public class WebRTCScreenCaptureActivity extends AppCompatActivity { + private static final String TAG = "WebRTCScreenCaptureActivity"; + // 请求码 + private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001; + private static final int REQUEST_CODE_PERMISSIONS = 1002; + + // WebRTC核心组件 + private PeerConnectionFactory peerConnectionFactory; + private PeerConnection peerConnection; + private VideoSource videoSource; + private VideoTrack videoTrack; + private EglBase rootEglBase; + + // 屏幕采集相关 + private MediaProjectionManager mediaProjectionManager; + private MediaProjection mediaProjection; + private VirtualDisplay virtualDisplay; + private ScreenVideoCapturer screenVideoCapturer; // 自定义屏幕采集器 + + // 屏幕参数 + private int screenWidth; + private int screenHeight; + private int screenDpi; + + // UI控件 + private Button btnStartPush; + private SurfaceViewRenderer localRender; // 本地预览(可选) + + private TextureView textureView; + + private VirtualDisplay mVirtualDisplay; + + // 远端信令信息(示例:实际需从信令服务器获取) + private String remoteSdp = ""; // 替换为远端实际SDP + private List remoteIceCandidates = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_webrtc_screen_capture); + + // 初始化控件 + btnStartPush = findViewById(R.id.btn_start_push); + localRender = findViewById(R.id.local_render); + textureView = findViewById(R.id.textureView); + + textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { + Log.e(TAG, "onSurfaceTextureAvailable: "); + // SurfaceTexture准备就绪,开始屏幕捕捉 + setupVirtualDisplay(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + Log.e(TAG, "onSurfaceTextureSizeChanged: "); + } + + @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); + + // 检查权限 + btnStartPush.setOnClickListener(v -> checkPermissionsAndStart()); + + // 初始化WebRTC EGL环境 + rootEglBase = EglBase.create(); + localRender.init(rootEglBase.getEglBaseContext(), null); + localRender.setMirror(true); + localRender.setEnableHardwareScaler(true); + } + + private static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"; + + /** + * 检查权限并启动采集+推流 + */ + 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); + } else { + // 启动前台服务 + startForegroundService(); + // 权限已满足,请求屏幕录制授权 + startScreenCaptureAuthorization(); + } + } + + /** + * 发起屏幕录制授权请求 + */ + private void startScreenCaptureAuthorization() { + Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent(); + startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE); + } + + /** + * 初始化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 + 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; + } + + private void sendSdpToRemote(SessionDescription sdp) { + // TODO: 实现信令发送逻辑,例如通过WebSocket发送SDP字符串 + } + + /** + * 自定义屏幕视频采集器(核心:对接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; + } + } + + /** + * 发送ICE候选到远端(示例:实际需通过WebSocket/HTTP信令服务器) + */ + private void sendIceCandidateToRemote(IceCandidate iceCandidate) { + // 这里仅做日志打印,实际需替换为信令发送逻辑 + Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp); + // 示例:将iceCandidate转为JSON发送到远端服务器 + } + + /** + * 处理权限请求结果 + */ + @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(); +// } + + // 重置按钮 + 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 e4b2611..a505c2e 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 @@ -12,13 +12,14 @@ 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 { - private static final String TAG ="MainActivity"; + private static final String TAG = "MainActivity"; private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897; private MediaProjectionManager mMediaProjectionManager; @@ -64,7 +65,7 @@ public class MainActivity extends BaseMvvmActivity= Build.VERSION_CODES.O) { startForegroundService(screenServiceIntent); - }else { + } else { startService(screenServiceIntent); } @@ -77,7 +78,7 @@ public class MainActivity extends BaseMvvmActivity= 33) { // if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { @@ -85,8 +86,10 @@ public class MainActivity extends BaseMvvmActivity= 31) { // Android 12+ 必须指定前台服务类型 - startForeground(NOTIFICATION_ID, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); } else { // 低版本直接启动 startForeground(NOTIFICATION_ID, notification); diff --git a/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java new file mode 100644 index 0000000..ef354e8 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService2.java @@ -0,0 +1,67 @@ +package com.ttstd.remoteservice.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +public class ScreenCaptureService2 extends Service { + private static final String TAG = "ScreenCaptureService2"; + + private static final String CHANNEL_ID = "ScreenCaptureChannel"; + private static final int NOTIFICATION_ID = 1001; + + @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()); + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + 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( + CHANNEL_ID, + "屏幕采集服务", + NotificationManager.IMPORTANCE_LOW + ); + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(channel); + } + } + + private Notification createNotification() { + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("屏幕采集中") + .setContentText("正在将屏幕内容推流到WebRTC") + .setSmallIcon(android.R.drawable.ic_media_play) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_webrtc_screen_capture.xml b/app/src/main/res/layout/activity_webrtc_screen_capture.xml new file mode 100644 index 0000000..d371f2d --- /dev/null +++ b/app/src/main/res/layout/activity_webrtc_screen_capture.xml @@ -0,0 +1,26 @@ + + + +