From 87f98799f70417d6e1e2ec8ef10202eaf9a6b819 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Mon, 13 Jul 2026 10:33:17 +0800 Subject: [PATCH] init --- WebRTCControlled/.gitignore | 2 + WebRTCControlled/app/build.gradle | 51 ++++ WebRTCControlled/app/proguard-rules.pro | 1 + .../app/src/main/AndroidManifest.xml | 34 +++ .../java/com/tt/controlled/MainActivity.java | 135 +++++++++ .../controlled/input/InputCommandHandler.java | 106 +++++++ .../service/ScreenCaptureService.java | 246 ++++++++++++++++ .../controlled/signaling/SignalMessage.java | 34 +++ .../controlled/signaling/WebSocketClient.java | 117 ++++++++ .../tt/controlled/webrtc/AppSdpObserver.java | 48 ++++ .../tt/controlled/webrtc/PeerObserver.java | 77 +++++ .../tt/controlled/webrtc/WebRtcClient.java | 254 +++++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 67 +++++ .../app/src/main/res/values/strings.xml | 4 + WebRTCControlled/build.gradle | 4 + WebRTCControlled/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + WebRTCControlled/gradlew | 172 +++++++++++ WebRTCControlled/gradlew.bat | 84 ++++++ WebRTCControlled/local.properties | 8 + WebRTCControlled/settings.gradle | 18 ++ WebRTCController/.gitignore | 2 + WebRTCController/app/build.gradle | 43 +++ WebRTCController/app/proguard-rules.pro | 1 + .../app/src/main/AndroidManifest.xml | 26 ++ .../java/com/tt/controller/MainActivity.java | 267 ++++++++++++++++++ .../controller/signaling/SignalMessage.java | 34 +++ .../controller/signaling/WebSocketClient.java | 116 ++++++++ .../tt/controller/view/RemoteTouchView.java | 110 ++++++++ .../tt/controller/webrtc/AppSdpObserver.java | 48 ++++ .../tt/controller/webrtc/PeerObserver.java | 77 +++++ .../tt/controller/webrtc/VideoRenderer.java | 35 +++ .../tt/controller/webrtc/WebRtcClient.java | 258 +++++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 124 ++++++++ .../app/src/main/res/values/strings.xml | 4 + WebRTCController/build.gradle | 4 + WebRTCController/gradle.properties | 3 + WebRTCController/local.properties | 8 + WebRTCController/settings.gradle | 18 ++ WebRTCSignalServer/.gitignore | 2 + WebRTCSignalServer/pom.xml | 52 ++++ .../tt/signaling/SignalServerApplication.java | 12 + .../tt/signaling/config/WebSocketConfig.java | 24 ++ .../handler/SignalWebSocketHandler.java | 155 ++++++++++ .../tt/signaling/manager/SessionManager.java | 70 +++++ .../com/tt/signaling/model/DeviceType.java | 6 + .../com/tt/signaling/model/SignalMessage.java | 30 ++ .../src/main/resources/application.yml | 11 + 49 files changed, 3011 insertions(+) create mode 100644 WebRTCControlled/.gitignore create mode 100644 WebRTCControlled/app/build.gradle create mode 100644 WebRTCControlled/app/proguard-rules.pro create mode 100644 WebRTCControlled/app/src/main/AndroidManifest.xml create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/MainActivity.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/input/InputCommandHandler.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/service/ScreenCaptureService.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/SignalMessage.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/WebSocketClient.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/AppSdpObserver.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/PeerObserver.java create mode 100644 WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/WebRtcClient.java create mode 100644 WebRTCControlled/app/src/main/res/layout/activity_main.xml create mode 100644 WebRTCControlled/app/src/main/res/values/strings.xml create mode 100644 WebRTCControlled/build.gradle create mode 100644 WebRTCControlled/gradle.properties create mode 100644 WebRTCControlled/gradle/wrapper/gradle-wrapper.jar create mode 100644 WebRTCControlled/gradle/wrapper/gradle-wrapper.properties create mode 100644 WebRTCControlled/gradlew create mode 100644 WebRTCControlled/gradlew.bat create mode 100644 WebRTCControlled/local.properties create mode 100644 WebRTCControlled/settings.gradle create mode 100644 WebRTCController/.gitignore create mode 100644 WebRTCController/app/build.gradle create mode 100644 WebRTCController/app/proguard-rules.pro create mode 100644 WebRTCController/app/src/main/AndroidManifest.xml create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/MainActivity.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/signaling/SignalMessage.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/signaling/WebSocketClient.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/view/RemoteTouchView.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/webrtc/AppSdpObserver.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/webrtc/PeerObserver.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/webrtc/VideoRenderer.java create mode 100644 WebRTCController/app/src/main/java/com/tt/controller/webrtc/WebRtcClient.java create mode 100644 WebRTCController/app/src/main/res/layout/activity_main.xml create mode 100644 WebRTCController/app/src/main/res/values/strings.xml create mode 100644 WebRTCController/build.gradle create mode 100644 WebRTCController/gradle.properties create mode 100644 WebRTCController/local.properties create mode 100644 WebRTCController/settings.gradle create mode 100644 WebRTCSignalServer/.gitignore create mode 100644 WebRTCSignalServer/pom.xml create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/SignalServerApplication.java create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/config/WebSocketConfig.java create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/handler/SignalWebSocketHandler.java create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/manager/SessionManager.java create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/model/DeviceType.java create mode 100644 WebRTCSignalServer/src/main/java/com/tt/signaling/model/SignalMessage.java create mode 100644 WebRTCSignalServer/src/main/resources/application.yml diff --git a/WebRTCControlled/.gitignore b/WebRTCControlled/.gitignore new file mode 100644 index 0000000..add2a72 --- /dev/null +++ b/WebRTCControlled/.gitignore @@ -0,0 +1,2 @@ +/build/ +/.idea/ \ No newline at end of file diff --git a/WebRTCControlled/app/build.gradle b/WebRTCControlled/app/build.gradle new file mode 100644 index 0000000..bc4775a --- /dev/null +++ b/WebRTCControlled/app/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.tt.controlled' + compileSdk 34 + + defaultConfig { + applicationId "com.tt.controlled" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + dataBinding true + buildConfig true + aidl true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // WebRTC + implementation 'io.github.nicholasgasior:webrtc-android:1.0.0' + // OkHttp for WebSocket + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + // Gson for JSON + implementation 'com.google.code.gson:gson:2.10.1' +} diff --git a/WebRTCControlled/app/proguard-rules.pro b/WebRTCControlled/app/proguard-rules.pro new file mode 100644 index 0000000..fb164d6 --- /dev/null +++ b/WebRTCControlled/app/proguard-rules.pro @@ -0,0 +1 @@ +# Add project specific ProGuard rules here. diff --git a/WebRTCControlled/app/src/main/AndroidManifest.xml b/WebRTCControlled/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ceb4057 --- /dev/null +++ b/WebRTCControlled/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/MainActivity.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/MainActivity.java new file mode 100644 index 0000000..3f96217 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/MainActivity.java @@ -0,0 +1,135 @@ +package com.tt.controlled; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +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.tt.controlled.service.ScreenCaptureService; + +import java.util.UUID; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "ControlledMain"; + private static final int REQUEST_MEDIA_PROJECTION = 1001; + private static final int REQUEST_NOTIFICATION_PERMISSION = 1002; + + private EditText etServerUrl; + private EditText etDeviceId; + private Button btnStart; + private Button btnStop; + private TextView tvStatus; + + private boolean isServiceRunning = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + etServerUrl = findViewById(R.id.et_server_url); + etDeviceId = findViewById(R.id.et_device_id); + btnStart = findViewById(R.id.btn_start); + btnStop = findViewById(R.id.btn_stop); + tvStatus = findViewById(R.id.tv_status); + + // 默认服务器地址 + etServerUrl.setText("ws://192.168.1.100:8080/ws/signal"); + // 生成设备ID + etDeviceId.setText("controlled_" + UUID.randomUUID().toString().substring(0, 8)); + + btnStart.setOnClickListener(v -> startScreenSharing()); + btnStop.setOnClickListener(v -> stopScreenSharing()); + + updateUI(false); + } + + private void startScreenSharing() { + // 检查通知权限 (Android 13+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_NOTIFICATION_PERMISSION); + return; + } + } + + // 请求屏幕录制权限 + MediaProjectionManager projectionManager = + (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); + startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_MEDIA_PROJECTION) { + if (resultCode == RESULT_OK && data != null) { + Intent serviceIntent = new Intent(this, ScreenCaptureService.class); + serviceIntent.putExtra(ScreenCaptureService.EXTRA_RESULT_CODE, resultCode); + serviceIntent.putExtra(ScreenCaptureService.EXTRA_RESULT_DATA, data); + serviceIntent.putExtra(ScreenCaptureService.EXTRA_SERVER_URL, etServerUrl.getText().toString()); + serviceIntent.putExtra(ScreenCaptureService.EXTRA_DEVICE_ID, etDeviceId.getText().toString()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent); + } else { + startService(serviceIntent); + } + + updateUI(true); + Toast.makeText(this, "屏幕共享已启动", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "屏幕录制权限被拒绝", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_NOTIFICATION_PERMISSION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startScreenSharing(); + } else { + Toast.makeText(this, "需要通知权限才能运行", Toast.LENGTH_SHORT).show(); + } + } + } + + private void stopScreenSharing() { + Intent serviceIntent = new Intent(this, ScreenCaptureService.class); + stopService(serviceIntent); + updateUI(false); + Toast.makeText(this, "屏幕共享已停止", Toast.LENGTH_SHORT).show(); + } + + private void updateUI(boolean running) { + isServiceRunning = running; + btnStart.setEnabled(!running); + btnStop.setEnabled(running); + etServerUrl.setEnabled(!running); + etDeviceId.setEnabled(!running); + tvStatus.setText(running ? "状态: 运行中" : "状态: 已停止"); + } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/input/InputCommandHandler.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/input/InputCommandHandler.java new file mode 100644 index 0000000..cd3e0c6 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/input/InputCommandHandler.java @@ -0,0 +1,106 @@ +package com.tt.controlled.input; + +import android.os.SystemClock; +import android.util.Log; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.MotionEvent; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.lang.reflect.Method; + +/** + * 处理控制端发来的输入指令(触摸、按键)。 + * 注意:注入输入事件需要 INJECT_EVENTS 权限, + * 通常需要系统签名或ADB授权才能使用。 + */ +public class InputCommandHandler { + + private static final String TAG = "InputCommandHandler"; + private final Gson gson = new Gson(); + private final int screenWidth; + private final int screenHeight; + + public InputCommandHandler(int screenWidth, int screenHeight) { + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + } + + public void handleCommand(String commandJson) { + try { + JsonObject command = gson.fromJson(commandJson, JsonObject.class); + String action = command.get("action").getAsString(); + + switch (action) { + case "TOUCH": + handleTouch(command); + break; + case "KEY": + handleKey(command); + break; + case "SWIPE": + handleSwipe(command); + break; + default: + Log.w(TAG, "Unknown action: " + action); + } + } catch (Exception e) { + Log.e(TAG, "Error handling command: " + e.getMessage(), e); + } + } + + private void handleTouch(JsonObject command) { + float relX = command.get("x").getAsFloat(); + float relY = command.get("y").getAsFloat(); + int x = (int) (relX * screenWidth); + int y = (int) (relY * screenHeight); + + Log.d(TAG, "Touch at: " + x + ", " + y); + injectTap(x, y); + } + + private void handleKey(JsonObject command) { + int keyCode = command.get("keyCode").getAsInt(); + Log.d(TAG, "Key press: " + keyCode); + // 使用 Runtime.exec 模拟按键(需要root或ADB权限) + try { + Runtime.getRuntime().exec("input keyevent " + keyCode); + } catch (Exception e) { + Log.e(TAG, "Error injecting key event", e); + } + } + + private void handleSwipe(JsonObject command) { + float relX1 = command.get("x1").getAsFloat(); + float relY1 = command.get("y1").getAsFloat(); + float relX2 = command.get("x2").getAsFloat(); + float relY2 = command.get("y2").getAsFloat(); + long duration = command.has("duration") ? command.get("duration").getAsLong() : 300; + + int x1 = (int) (relX1 * screenWidth); + int y1 = (int) (relY1 * screenHeight); + int x2 = (int) (relX2 * screenWidth); + int y2 = (int) (relY2 * screenHeight); + + Log.d(TAG, "Swipe from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")"); + injectSwipe(x1, y1, x2, y2, duration); + } + + private void injectTap(int x, int y) { + try { + Runtime.getRuntime().exec("input tap " + x + " " + y); + } catch (Exception e) { + Log.e(TAG, "Error injecting tap event", e); + } + } + + private void injectSwipe(int x1, int y1, int x2, int y2, long duration) { + try { + Runtime.getRuntime().exec("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + duration); + } catch (Exception e) { + Log.e(TAG, "Error injecting swipe event", e); + } + } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/service/ScreenCaptureService.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/service/ScreenCaptureService.java new file mode 100644 index 0000000..d8ab7ef --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/service/ScreenCaptureService.java @@ -0,0 +1,246 @@ +package com.tt.controlled.service; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; +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.WindowManager; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import com.tt.controlled.MainActivity; +import com.tt.controlled.input.InputCommandHandler; +import com.tt.controlled.signaling.SignalMessage; +import com.tt.controlled.signaling.WebSocketClient; +import com.tt.controlled.webrtc.WebRtcClient; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.ScreenCapturerAndroid; +import org.webrtc.SessionDescription; + +public class ScreenCaptureService extends Service { + + private static final String TAG = "ScreenCaptureService"; + private static final String CHANNEL_ID = "screen_capture_channel"; + private static final int NOTIFICATION_ID = 1; + + public static final String EXTRA_RESULT_CODE = "result_code"; + public static final String EXTRA_RESULT_DATA = "result_data"; + public static final String EXTRA_SERVER_URL = "server_url"; + public static final String EXTRA_DEVICE_ID = "device_id"; + + private MediaProjection mediaProjection; + private ScreenCapturerAndroid screenCapturer; + private WebSocketClient wsClient; + private WebRtcClient webRtcClient; + private InputCommandHandler inputHandler; + private EglBase eglBase; + private final Gson gson = new Gson(); + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + stopSelf(); + return START_NOT_STICKY; + } + + // 创建前台通知 + Notification notification = createNotification(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); + } else { + startForeground(NOTIFICATION_ID, notification); + } + + int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED); + Intent resultData = intent.getParcelableExtra(EXTRA_RESULT_DATA); + String serverUrl = intent.getStringExtra(EXTRA_SERVER_URL); + String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID); + + if (resultCode != Activity.RESULT_OK || resultData == null || serverUrl == null || deviceId == null) { + Log.e(TAG, "Invalid service parameters"); + stopSelf(); + return START_NOT_STICKY; + } + + // 获取屏幕尺寸 + WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(dm); + inputHandler = new InputCommandHandler(dm.widthPixels, dm.heightPixels); + + startScreenCapture(resultCode, resultData, serverUrl, deviceId); + return START_NOT_STICKY; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (screenCapturer != null) { + screenCapturer.stopCapture(); + screenCapturer.dispose(); + } + if (webRtcClient != null) { + webRtcClient.close(); + } + if (wsClient != null) { + wsClient.disconnect(); + } + if (eglBase != null) { + eglBase.release(); + } + if (mediaProjection != null) { + mediaProjection.stop(); + } + } + + private void startScreenCapture(int resultCode, Intent resultData, String serverUrl, String deviceId) { + // 初始化 MediaProjection + MediaProjectionManager projectionManager = + (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); + mediaProjection = projectionManager.getMediaProjection(resultCode, resultData); + + // 初始化 EGL + eglBase = EglBase.create(); + + // 初始化 WebSocket + wsClient = new WebSocketClient(serverUrl, deviceId); + wsClient.setListener(new WebSocketClient.SignalListener() { + @Override + public void onConnected() { + Log.i(TAG, "Connected to signal server"); + } + + @Override + public void onDisconnected() { + Log.i(TAG, "Disconnected from signal server"); + } + + @Override + public void onError(String error) { + Log.e(TAG, "WebSocket error: " + error); + } + + @Override + public void onMessage(SignalMessage message) { + handleSignalMessage(message); + } + }); + wsClient.connect(); + + // 初始化 WebRTC + webRtcClient = new WebRtcClient(this, wsClient, deviceId); + webRtcClient.initialize(eglBase); + webRtcClient.setInputCallback(commandJson -> inputHandler.handleCommand(commandJson)); + + // 初始化屏幕捕获 + screenCapturer = new ScreenCapturerAndroid(resultData, new ScreenCapturerAndroid.Callback() { + @Override + public void onCapturerStarted(boolean success) { + Log.d(TAG, "Screen capture started: " + success); + } + + @Override + public void onCapturerStopped() { + Log.d(TAG, "Screen capture stopped"); + } + }); + + webRtcClient.setVideoCapturer(screenCapturer); + } + + private void handleSignalMessage(SignalMessage message) { + String type = message.getType(); + if (type == null) return; + + switch (type.toUpperCase()) { + case "OFFER": + handleOffer(message); + break; + case "ICE_CANDIDATE": + handleIceCandidate(message); + break; + } + } + + private void handleOffer(SignalMessage message) { + String offerSdp = null; + try { + JsonObject payload = gson.fromJson(message.getPayload(), JsonObject.class); + offerSdp = payload.get("sdp").getAsString(); + } catch (Exception e) { + Log.e(TAG, "Error parsing offer SDP", e); + return; + } + + String controllerId = message.getFromDeviceId(); + webRtcClient.createPeerConnectionAndAnswer(offerSdp, controllerId); + } + + private void handleIceCandidate(SignalMessage message) { + try { + JsonObject payload = gson.fromJson(message.getPayload(), JsonObject.class); + String sdpMid = payload.get("sdpMid").getAsString(); + int sdpMLineIndex = payload.get("sdpMLineIndex").getAsInt(); + String candidate = payload.get("candidate").getAsString(); + + IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate); + webRtcClient.addIceCandidate(iceCandidate); + } catch (Exception e) { + Log.e(TAG, "Error parsing ICE candidate", e); + } + } + + private Notification createNotification() { + Intent notificationIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("屏幕共享中") + .setContentText("远程控制服务正在运行") + .setSmallIcon(android.R.drawable.ic_menu_camera) + .setContentIntent(pendingIntent) + .build(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "屏幕共享服务", + NotificationManager.IMPORTANCE_LOW); + channel.setDescription("远程控制屏幕采集前台服务"); + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(channel); + } + } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/SignalMessage.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/SignalMessage.java new file mode 100644 index 0000000..483dff3 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/SignalMessage.java @@ -0,0 +1,34 @@ +package com.tt.controlled.signaling; + +public class SignalMessage { + + private String type; + private String fromDeviceId; + private String toDeviceId; + private String deviceType; + private String payload; + + public SignalMessage() {} + + public SignalMessage(String type, String fromDeviceId, String toDeviceId, String payload) { + this.type = type; + this.fromDeviceId = fromDeviceId; + this.toDeviceId = toDeviceId; + this.payload = payload; + } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getFromDeviceId() { return fromDeviceId; } + public void setFromDeviceId(String fromDeviceId) { this.fromDeviceId = fromDeviceId; } + + public String getToDeviceId() { return toDeviceId; } + public void setToDeviceId(String toDeviceId) { this.toDeviceId = toDeviceId; } + + public String getDeviceType() { return deviceType; } + public void setDeviceType(String deviceType) { this.deviceType = deviceType; } + + public String getPayload() { return payload; } + public void setPayload(String payload) { this.payload = payload; } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/WebSocketClient.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/WebSocketClient.java new file mode 100644 index 0000000..95fa441 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/signaling/WebSocketClient.java @@ -0,0 +1,117 @@ +package com.tt.controlled.signaling; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.google.gson.Gson; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +public class WebSocketClient { + + private static final String TAG = "WebSocketClient"; + + private final String serverUrl; + private final String deviceId; + private final Gson gson = new Gson(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private OkHttpClient client; + private WebSocket webSocket; + private SignalListener listener; + + public interface SignalListener { + void onConnected(); + void onDisconnected(); + void onError(String error); + void onMessage(SignalMessage message); + } + + public WebSocketClient(String serverUrl, String deviceId) { + this.serverUrl = serverUrl; + this.deviceId = deviceId; + } + + public void setListener(SignalListener listener) { + this.listener = listener; + } + + public void connect() { + client = new OkHttpClient.Builder().build(); + Request request = new Request.Builder().url(serverUrl).build(); + + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response response) { + Log.i(TAG, "WebSocket connected"); + // 发送注册消息 + registerDevice(); + mainHandler.post(() -> { + if (listener != null) listener.onConnected(); + }); + } + + @Override + public void onMessage(WebSocket ws, String text) { + Log.d(TAG, "Received: " + text); + try { + SignalMessage message = gson.fromJson(text, SignalMessage.class); + mainHandler.post(() -> { + if (listener != null) listener.onMessage(message); + }); + } catch (Exception e) { + Log.e(TAG, "Error parsing message", e); + } + } + + @Override + public void onClosed(WebSocket ws, int code, String reason) { + Log.i(TAG, "WebSocket closed: " + reason); + mainHandler.post(() -> { + if (listener != null) listener.onDisconnected(); + }); + } + + @Override + public void onFailure(WebSocket ws, Throwable t, Response response) { + Log.e(TAG, "WebSocket error: " + t.getMessage(), t); + mainHandler.post(() -> { + if (listener != null) listener.onError(t.getMessage()); + }); + } + }); + } + + public void sendMessage(SignalMessage message) { + if (webSocket != null) { + String json = gson.toJson(message); + Log.d(TAG, "Sending: " + json); + webSocket.send(json); + } + } + + public void disconnect() { + if (webSocket != null) { + webSocket.close(1000, "Disconnecting"); + } + if (client != null) { + client.dispatcher().executorService().shutdown(); + } + } + + private void registerDevice() { + SignalMessage registerMsg = new SignalMessage(); + registerMsg.setType("REGISTER"); + registerMsg.setFromDeviceId(deviceId); + registerMsg.setDeviceType("CONTROLLED"); + sendMessage(registerMsg); + } + + public boolean isConnected() { + return webSocket != null; + } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/AppSdpObserver.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/AppSdpObserver.java new file mode 100644 index 0000000..60fbfc6 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/AppSdpObserver.java @@ -0,0 +1,48 @@ +package com.tt.controlled.webrtc; + +import android.util.Log; + +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; + +public class AppSdpObserver implements SdpObserver { + + private static final String TAG = "AppSdpObserver"; + + private final SdpEventListener listener; + + public interface SdpEventListener { + void onCreateSuccess(SessionDescription sdp); + void onSetSuccess(); + void onCreateFailure(String error); + void onSetFailure(String error); + } + + public AppSdpObserver(SdpEventListener listener) { + this.listener = listener; + } + + @Override + public void onCreateSuccess(SessionDescription sdp) { + Log.d(TAG, "SDP created: " + sdp.type); + if (listener != null) listener.onCreateSuccess(sdp); + } + + @Override + public void onSetSuccess() { + Log.d(TAG, "SDP set success"); + if (listener != null) listener.onSetSuccess(); + } + + @Override + public void onCreateFailure(String error) { + Log.e(TAG, "SDP create failure: " + error); + if (listener != null) listener.onCreateFailure(error); + } + + @Override + public void onSetFailure(String error) { + Log.e(TAG, "SDP set failure: " + error); + if (listener != null) listener.onSetFailure(error); + } +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/PeerObserver.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/PeerObserver.java new file mode 100644 index 0000000..5b89ca9 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/PeerObserver.java @@ -0,0 +1,77 @@ +package com.tt.controlled.webrtc; + +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.RtpReceiver; + +public class PeerObserver implements PeerConnection.Observer { + + private static final String TAG = "PeerObserver"; + + private final PeerEventListener listener; + + public interface PeerEventListener { + void onIceCandidate(IceCandidate candidate); + void onIceConnectionChange(PeerConnection.IceConnectionState newState); + void onDataChannel(DataChannel dataChannel); + void onAddStream(MediaStream stream); + void onRemoveStream(MediaStream stream); + } + + public PeerObserver(PeerEventListener listener) { + this.listener = listener; + } + + @Override + public void onSignalingChange(PeerConnection.SignalingState newState) { + android.util.Log.d(TAG, "Signaling state: " + newState); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState newState) { + android.util.Log.d(TAG, "ICE connection state: " + newState); + if (listener != null) listener.onIceConnectionChange(newState); + } + + @Override + public void onIceConnectionReceivingChange(boolean receiving) {} + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState newState) { + android.util.Log.d(TAG, "ICE gathering state: " + newState); + } + + @Override + public void onIceCandidate(IceCandidate candidate) { + if (listener != null) listener.onIceCandidate(candidate); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] candidates) {} + + @Override + public void onAddStream(MediaStream stream) { + if (listener != null) listener.onAddStream(stream); + } + + @Override + public void onRemoveStream(MediaStream stream) { + if (listener != null) listener.onRemoveStream(stream); + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + android.util.Log.d(TAG, "Remote data channel received"); + if (listener != null) listener.onDataChannel(dataChannel); + } + + @Override + public void onRenegotiationNeeded() { + android.util.Log.d(TAG, "Renegotiation needed"); + } + + @Override + public void onAddTrack(RtpReceiver receiver, MediaStream[] streams) {} +} diff --git a/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/WebRtcClient.java b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/WebRtcClient.java new file mode 100644 index 0000000..a86befe --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/tt/controlled/webrtc/WebRtcClient.java @@ -0,0 +1,254 @@ +package com.tt.controlled.webrtc; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.tt.controlled.signaling.SignalMessage; +import com.tt.controlled.signaling.WebSocketClient; + +import org.webrtc.DataChannel; +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.SessionDescription; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class WebRtcClient { + + private static final String TAG = "WebRtcClient"; + private static final String VIDEO_TRACK_ID = "screen_track"; + private static final String STREAM_ID = "screen_stream"; + private static final String DATA_CHANNEL_LABEL = "control_channel"; + + private final Context context; + private final WebSocketClient wsClient; + private final String deviceId; + private final Gson gson = new Gson(); + + private PeerConnectionFactory peerConnectionFactory; + private PeerConnection peerConnection; + private DataChannel dataChannel; + private VideoTrack videoTrack; + private EglBase eglBase; + private String currentControllerId; + + private InputCommandCallback inputCallback; + + public interface InputCommandCallback { + void onCommandReceived(String commandJson); + } + + public WebRtcClient(Context context, WebSocketClient wsClient, String deviceId) { + this.context = context; + this.wsClient = wsClient; + this.deviceId = deviceId; + } + + public void setInputCallback(InputCommandCallback callback) { + this.inputCallback = callback; + } + + public void initialize(EglBase eglBase) { + this.eglBase = eglBase; + PeerConnectionFactory.InitializationOptions initOptions = + PeerConnectionFactory.InitializationOptions.builder(context) + .setFieldTrials("WebRTC-H264HighProfile/Enabled/") + .createInitializationOptions(); + PeerConnectionFactory.initialize(initOptions); + + PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); + peerConnectionFactory = PeerConnectionFactory.builder() + .setOptions(options) + .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) + .createPeerConnectionFactory(); + } + + public void setVideoCapturer(VideoCapturer capturer) { + VideoSource videoSource = peerConnectionFactory.createVideoSource(false); + capturer.initialize(null, context, videoSource.getCapturerObserver()); + capturer.startCapture(1280, 720, 30); + + videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource); + } + + public void createPeerConnectionAndAnswer(String offerSdp, String controllerId) { + this.currentControllerId = controllerId; + + PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(new ArrayList<>()); + config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + + PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() { + @Override + public void onIceCandidate(IceCandidate candidate) { + sendIceCandidate(candidate, controllerId); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState newState) { + Log.d(TAG, "ICE connection state: " + newState); + } + + @Override + public void onDataChannel(DataChannel dc) { + Log.d(TAG, "Data channel received: " + dc.label()); + handleDataChannel(dc); + } + + @Override + public void onAddStream(MediaStream stream) {} + + @Override + public void onRemoveStream(MediaStream stream) {} + }); + + peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver); + + if (videoTrack != null) { + MediaStream stream = peerConnectionFactory.createLocalMediaStream(STREAM_ID); + stream.addTrack(videoTrack); + peerConnection.addStream(stream); + } + + // 创建本地 DataChannel + DataChannel.Init dcInit = new DataChannel.Init(); + dcInit.ordered = true; + dataChannel = peerConnection.createDataChannel(DATA_CHANNEL_LABEL, dcInit); + handleDataChannel(dataChannel); + + // 设置远端 SDP (Offer) + SessionDescription remoteSdp = new SessionDescription(SessionDescription.Type.OFFER, offerSdp); + peerConnection.setRemoteDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() { + @Override + public void onCreateSuccess(SessionDescription sdp) {} + + @Override + public void onSetSuccess() { + // 创建 Answer + MediaConstraints constraints = new MediaConstraints(); + peerConnection.createAnswer(new AppSdpObserver(new AppSdpObserver.SdpEventListener() { + @Override + public void onCreateSuccess(SessionDescription sdp) { + peerConnection.setLocalDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() { + @Override + public void onCreateSuccess(SessionDescription s) {} + @Override + public void onSetSuccess() { + sendAnswer(sdp.description, controllerId); + } + @Override + public void onCreateFailure(String error) {} + @Override + public void onSetFailure(String error) {} + }), sdp); + } + + @Override + public void onSetSuccess() {} + @Override + public void onCreateFailure(String error) {} + @Override + public void onSetFailure(String error) {} + }), constraints); + } + + @Override + public void onCreateFailure(String error) {} + @Override + public void onSetFailure(String error) {} + }), remoteSdp); + } + + public void addIceCandidate(IceCandidate candidate) { + if (peerConnection != null) { + peerConnection.addIceCandidate(candidate); + } + } + + public void sendInputResponse(String responseJson) { + if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN) { + DataChannel.Buffer buffer = new DataChannel.Buffer( + ByteBuffer.wrap(responseJson.getBytes(StandardCharsets.UTF_8)), false); + dataChannel.send(buffer); + } + } + + public void close() { + if (dataChannel != null) { + dataChannel.close(); + dataChannel.dispose(); + } + if (peerConnection != null) { + peerConnection.close(); + peerConnection.dispose(); + } + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } + } + + private void handleDataChannel(DataChannel dc) { + dc.registerObserver(new DataChannel.Observer() { + @Override + public void onBufferedAmountChange(long previousAmount) {} + + @Override + public void onStateChange() { + Log.d(TAG, "DataChannel state: " + dc.state()); + } + + @Override + public void onMessage(DataChannel.Buffer buffer) { + byte[] data = new byte[buffer.data.remaining()]; + buffer.data.get(data); + String message = new String(data, StandardCharsets.UTF_8); + Log.d(TAG, "DataChannel message: " + message); + if (inputCallback != null) { + inputCallback.onCommandReceived(message); + } + } + }); + } + + private void sendAnswer(String sdp, String controllerId) { + SignalMessage msg = new SignalMessage(); + msg.setType("ANSWER"); + msg.setFromDeviceId(deviceId); + msg.setToDeviceId(controllerId); + msg.setDeviceType("CONTROLLED"); + + JsonObject payload = new JsonObject(); + payload.addProperty("sdp", sdp); + msg.setPayload(payload.toString()); + + wsClient.sendMessage(msg); + } + + private void sendIceCandidate(IceCandidate candidate, String controllerId) { + SignalMessage msg = new SignalMessage(); + msg.setType("ICE_CANDIDATE"); + msg.setFromDeviceId(deviceId); + msg.setToDeviceId(controllerId); + msg.setDeviceType("CONTROLLED"); + + JsonObject payload = new JsonObject(); + payload.addProperty("sdpMid", candidate.sdpMid); + payload.addProperty("sdpMLineIndex", candidate.sdpMLineIndex); + payload.addProperty("candidate", candidate.sdp); + msg.setPayload(payload.toString()); + + wsClient.sendMessage(msg); + } +} diff --git a/WebRTCControlled/app/src/main/res/layout/activity_main.xml b/WebRTCControlled/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..bbc4074 --- /dev/null +++ b/WebRTCControlled/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + +