feat: 增加预览并远程显示画面
This commit is contained in:
1
WebRTCControlled/.gitignore
vendored
1
WebRTCControlled/.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/app/build/
|
/app/build/
|
||||||
/.idea/
|
/.idea/
|
||||||
/.gradle/
|
/.gradle/
|
||||||
|
config.gradle
|
||||||
@@ -2,6 +2,14 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def releaseTime() {
|
||||||
|
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
def appName() {
|
||||||
|
return "WebRTCControlled"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.tt.controlled'
|
namespace 'com.tt.controlled'
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
@@ -25,10 +33,85 @@ android {
|
|||||||
aidl true
|
aidl true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
keypub {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.keypub.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.keypub.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.keypub.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.keypub.keyPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshatch {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.crosshatch.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.crosshatch.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.crosshatch.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.crosshatch.keyPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
zhanxun {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.zhanxun.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.zhanxun.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.zhanxun.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.zhanxun.keyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
zhanRuiDebug.initWith(debug)
|
||||||
|
zhanRuiDebug {
|
||||||
|
debuggable true
|
||||||
|
signingConfig signingConfigs.zhanxun
|
||||||
|
}
|
||||||
|
|
||||||
|
zhanRuiRelease.initWith(release)
|
||||||
|
zhanRuiRelease {
|
||||||
|
signingConfig signingConfigs.zhanxun
|
||||||
|
}
|
||||||
|
|
||||||
|
CrosshatchDebug.initWith(debug)
|
||||||
|
CrosshatchDebug {
|
||||||
|
debuggable true
|
||||||
|
signingConfig signingConfigs.crosshatch
|
||||||
|
}
|
||||||
|
|
||||||
|
CrosshatchRelease.initWith(release)
|
||||||
|
CrosshatchRelease {
|
||||||
|
signingConfig signingConfigs.crosshatch
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
versionNameSuffix "_debug"
|
||||||
|
debuggable true
|
||||||
|
minifyEnabled false
|
||||||
|
//Zipalign优化
|
||||||
|
zipAlignEnabled true
|
||||||
|
signingConfig signingConfigs.keypub
|
||||||
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
//Zipalign优化
|
||||||
|
zipAlignEnabled true
|
||||||
|
//前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
//签名
|
||||||
|
signingConfig signingConfigs.keypub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add application variant configuration here instead
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
def buildType = variant.buildType.name
|
||||||
|
def fileName = ""
|
||||||
|
|
||||||
|
if (buildType.contains("debug")) {
|
||||||
|
fileName = "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk"
|
||||||
|
} else {
|
||||||
|
fileName = "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk"
|
||||||
|
}
|
||||||
|
|
||||||
|
output.outputFileName = fileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:sharedUserId="android.uid.system">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.MaterialComponents.DayNight">
|
android:theme="@style/Theme.MaterialComponents.DayNight">
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.tt.controlled;
|
package com.tt.controlled;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.media.projection.MediaProjectionManager;
|
import android.media.projection.MediaProjectionManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -22,6 +25,10 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.tt.controlled.service.ScreenCaptureService;
|
import com.tt.controlled.service.ScreenCaptureService;
|
||||||
|
import com.tt.controlled.webrtc.WebRtcClient;
|
||||||
|
|
||||||
|
import org.webrtc.RendererCommon;
|
||||||
|
import org.webrtc.SurfaceViewRenderer;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -36,8 +43,28 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private Button btnStart;
|
private Button btnStart;
|
||||||
private Button btnStop;
|
private Button btnStop;
|
||||||
private TextView tvStatus;
|
private TextView tvStatus;
|
||||||
|
private SurfaceViewRenderer localView;
|
||||||
|
|
||||||
private boolean isServiceRunning = false;
|
private boolean isServiceRunning = false;
|
||||||
|
private ScreenCaptureService screenCaptureService;
|
||||||
|
private boolean isBound = false;
|
||||||
|
private boolean isLocalViewInitialized = false;
|
||||||
|
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
ScreenCaptureService.LocalBinder binder = (ScreenCaptureService.LocalBinder) service;
|
||||||
|
screenCaptureService = binder.getService();
|
||||||
|
isBound = true;
|
||||||
|
setupLocalPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
isBound = false;
|
||||||
|
screenCaptureService = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -49,11 +76,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
btnStart = findViewById(R.id.btn_start);
|
btnStart = findViewById(R.id.btn_start);
|
||||||
btnStop = findViewById(R.id.btn_stop);
|
btnStop = findViewById(R.id.btn_stop);
|
||||||
tvStatus = findViewById(R.id.tv_status);
|
tvStatus = findViewById(R.id.tv_status);
|
||||||
|
localView = findViewById(R.id.local_view);
|
||||||
|
|
||||||
// 默认服务器地址
|
// 默认服务器地址
|
||||||
etServerUrl.setText("ws://192.168.1.100:8080/ws/signal");
|
etServerUrl.setText("ws://192.168.100.222:8080/ws/signal");
|
||||||
// 生成设备ID
|
// 生成设备ID
|
||||||
etDeviceId.setText("controlled_" + UUID.randomUUID().toString().substring(0, 8));
|
// etDeviceId.setText("controlled_" + UUID.randomUUID().toString().substring(0, 8));
|
||||||
|
|
||||||
btnStart.setOnClickListener(v -> startScreenSharing());
|
btnStart.setOnClickListener(v -> startScreenSharing());
|
||||||
btnStop.setOnClickListener(v -> stopScreenSharing());
|
btnStop.setOnClickListener(v -> stopScreenSharing());
|
||||||
@@ -97,6 +125,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
startService(serviceIntent);
|
startService(serviceIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||||
|
|
||||||
updateUI(true);
|
updateUI(true);
|
||||||
Toast.makeText(this, "屏幕共享已启动", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "屏幕共享已启动", Toast.LENGTH_SHORT).show();
|
||||||
} else {
|
} else {
|
||||||
@@ -118,12 +148,53 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void stopScreenSharing() {
|
private void stopScreenSharing() {
|
||||||
|
if (isBound) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
isBound = false;
|
||||||
|
}
|
||||||
Intent serviceIntent = new Intent(this, ScreenCaptureService.class);
|
Intent serviceIntent = new Intent(this, ScreenCaptureService.class);
|
||||||
stopService(serviceIntent);
|
stopService(serviceIntent);
|
||||||
|
if (localView != null) {
|
||||||
|
localView.release();
|
||||||
|
isLocalViewInitialized = false;
|
||||||
|
}
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
Toast.makeText(this, "屏幕共享已停止", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "屏幕共享已停止", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupLocalPreview() {
|
||||||
|
if (isBound && screenCaptureService != null && !isLocalViewInitialized) {
|
||||||
|
WebRtcClient webRtcClient = screenCaptureService.getWebRtcClient();
|
||||||
|
if (webRtcClient != null && webRtcClient.getEglContext() != null) {
|
||||||
|
try {
|
||||||
|
localView.init(webRtcClient.getEglContext(), null);
|
||||||
|
localView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
||||||
|
localView.setMirror(false);
|
||||||
|
webRtcClient.setLocalSink(localView);
|
||||||
|
isLocalViewInitialized = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing local view", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果 webRtcClient 还没准备好,稍后重试
|
||||||
|
localView.postDelayed(this::setupLocalPreview, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (isBound) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
isBound = false;
|
||||||
|
}
|
||||||
|
if (localView != null) {
|
||||||
|
localView.release();
|
||||||
|
isLocalViewInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateUI(boolean running) {
|
private void updateUI(boolean running) {
|
||||||
isServiceRunning = running;
|
isServiceRunning = running;
|
||||||
btnStart.setEnabled(!running);
|
btnStart.setEnabled(!running);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.content.Intent;
|
|||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.media.projection.MediaProjection;
|
import android.media.projection.MediaProjection;
|
||||||
import android.media.projection.MediaProjectionManager;
|
import android.media.projection.MediaProjectionManager;
|
||||||
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
@@ -85,20 +86,50 @@ public class ScreenCaptureService extends Service {
|
|||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取屏幕尺寸
|
// 获取屏幕真实尺寸和刷新率
|
||||||
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
||||||
DisplayMetrics dm = new DisplayMetrics();
|
DisplayMetrics dm = new DisplayMetrics();
|
||||||
wm.getDefaultDisplay().getMetrics(dm);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
inputHandler = new InputCommandHandler(dm.widthPixels, dm.heightPixels);
|
wm.getDefaultDisplay().getRealMetrics(dm);
|
||||||
|
} else {
|
||||||
|
wm.getDefaultDisplay().getMetrics(dm);
|
||||||
|
}
|
||||||
|
int width = dm.widthPixels;
|
||||||
|
int height = dm.heightPixels;
|
||||||
|
|
||||||
startScreenCapture(resultCode, resultData, serverUrl, deviceId);
|
float refreshRate = 30;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
try {
|
||||||
|
refreshRate = wm.getDefaultDisplay().getRefreshRate();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
int fps = Math.round(refreshRate);
|
||||||
|
if (fps > 60) fps = 60; // 限制最大 60fps
|
||||||
|
if (fps <= 0) fps = 30;
|
||||||
|
|
||||||
|
Log.i(TAG, "Screen resolution: " + width + "x" + height + " @ " + fps + "fps");
|
||||||
|
inputHandler = new InputCommandHandler(width, height);
|
||||||
|
|
||||||
|
startScreenCapture(resultCode, resultData, serverUrl, deviceId, width, height, fps);
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final IBinder binder = new LocalBinder();
|
||||||
|
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
public ScreenCaptureService getService() {
|
||||||
|
return ScreenCaptureService.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return null;
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebRtcClient getWebRtcClient() {
|
||||||
|
return webRtcClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -122,7 +153,7 @@ public class ScreenCaptureService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startScreenCapture(int resultCode, Intent resultData, String serverUrl, String deviceId) {
|
private void startScreenCapture(int resultCode, Intent resultData, String serverUrl, String deviceId, int width, int height, int fps) {
|
||||||
// 初始化 MediaProjection
|
// 初始化 MediaProjection
|
||||||
MediaProjectionManager projectionManager =
|
MediaProjectionManager projectionManager =
|
||||||
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
||||||
@@ -166,7 +197,7 @@ public class ScreenCaptureService extends Service {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
webRtcClient.setVideoCapturer(screenCapturer);
|
webRtcClient.setVideoCapturer(screenCapturer, width, height, fps);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSignalMessage(SignalMessage message) {
|
private void handleSignalMessage(SignalMessage message) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Looper;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
@@ -18,7 +19,7 @@ public class WebSocketClient {
|
|||||||
|
|
||||||
private final String serverUrl;
|
private final String serverUrl;
|
||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
private OkHttpClient client;
|
private OkHttpClient client;
|
||||||
private WebSocket webSocket;
|
private WebSocket webSocket;
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import android.content.Context;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.tt.controlled.signaling.SignalMessage;
|
import com.tt.controlled.signaling.SignalMessage;
|
||||||
import com.tt.controlled.signaling.WebSocketClient;
|
import com.tt.controlled.signaling.WebSocketClient;
|
||||||
|
|
||||||
import org.webrtc.DataChannel;
|
import org.webrtc.DataChannel;
|
||||||
|
import org.webrtc.DefaultVideoDecoderFactory;
|
||||||
import org.webrtc.DefaultVideoEncoderFactory;
|
import org.webrtc.DefaultVideoEncoderFactory;
|
||||||
import org.webrtc.EglBase;
|
import org.webrtc.EglBase;
|
||||||
import org.webrtc.IceCandidate;
|
import org.webrtc.IceCandidate;
|
||||||
@@ -17,13 +19,17 @@ import org.webrtc.MediaStream;
|
|||||||
import org.webrtc.PeerConnection;
|
import org.webrtc.PeerConnection;
|
||||||
import org.webrtc.PeerConnectionFactory;
|
import org.webrtc.PeerConnectionFactory;
|
||||||
import org.webrtc.SessionDescription;
|
import org.webrtc.SessionDescription;
|
||||||
|
import org.webrtc.SurfaceTextureHelper;
|
||||||
import org.webrtc.VideoCapturer;
|
import org.webrtc.VideoCapturer;
|
||||||
|
import org.webrtc.VideoCodecInfo;
|
||||||
|
import org.webrtc.VideoSink;
|
||||||
import org.webrtc.VideoSource;
|
import org.webrtc.VideoSource;
|
||||||
import org.webrtc.VideoTrack;
|
import org.webrtc.VideoTrack;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class WebRtcClient {
|
public class WebRtcClient {
|
||||||
@@ -36,7 +42,7 @@ public class WebRtcClient {
|
|||||||
private final Context context;
|
private final Context context;
|
||||||
private final WebSocketClient wsClient;
|
private final WebSocketClient wsClient;
|
||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||||
|
|
||||||
private PeerConnectionFactory peerConnectionFactory;
|
private PeerConnectionFactory peerConnectionFactory;
|
||||||
private PeerConnection peerConnection;
|
private PeerConnection peerConnection;
|
||||||
@@ -65,30 +71,77 @@ public class WebRtcClient {
|
|||||||
this.eglBase = eglBase;
|
this.eglBase = eglBase;
|
||||||
PeerConnectionFactory.InitializationOptions initOptions =
|
PeerConnectionFactory.InitializationOptions initOptions =
|
||||||
PeerConnectionFactory.InitializationOptions.builder(context)
|
PeerConnectionFactory.InitializationOptions.builder(context)
|
||||||
.setFieldTrials("WebRTC-H264HighProfile/Enabled/")
|
.setFieldTrials("WebRTC-H264HighProfile/Enabled/WebRTC-Video-HwAcceleration/Enabled/")
|
||||||
.createInitializationOptions();
|
.createInitializationOptions();
|
||||||
PeerConnectionFactory.initialize(initOptions);
|
PeerConnectionFactory.initialize(initOptions);
|
||||||
|
|
||||||
|
DefaultVideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true);
|
||||||
|
DefaultVideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
|
||||||
|
|
||||||
|
// 打印所有支持的编码格式
|
||||||
|
Log.i(TAG, "Supported Encoders:");
|
||||||
|
for (VideoCodecInfo info : encoderFactory.getSupportedCodecs()) {
|
||||||
|
Log.i(TAG, " - Codec: " + info.name + ", params: " + info.params);
|
||||||
|
}
|
||||||
|
|
||||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
.setOptions(options)
|
.setOptions(options)
|
||||||
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true))
|
.setVideoEncoderFactory(encoderFactory)
|
||||||
|
.setVideoDecoderFactory(decoderFactory)
|
||||||
.createPeerConnectionFactory();
|
.createPeerConnectionFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVideoCapturer(VideoCapturer capturer) {
|
public void setVideoCapturer(VideoCapturer capturer, int width, int height, int fps) {
|
||||||
VideoSource videoSource = peerConnectionFactory.createVideoSource(false);
|
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
|
||||||
capturer.initialize(null, context, videoSource.getCapturerObserver());
|
// 对于屏幕共享,isScreencast 设为 true,这会优化细节保留
|
||||||
capturer.startCapture(1280, 720, 30);
|
VideoSource videoSource = peerConnectionFactory.createVideoSource(true);
|
||||||
|
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||||
|
capturer.startCapture(width, height, fps);
|
||||||
|
|
||||||
videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
|
videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
|
||||||
|
if (localSink != null) {
|
||||||
|
videoTrack.addSink(localSink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VideoSink localSink;
|
||||||
|
|
||||||
|
public void setLocalSink(VideoSink sink) {
|
||||||
|
this.localSink = sink;
|
||||||
|
if (videoTrack != null && sink != null) {
|
||||||
|
videoTrack.addSink(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EglBase.Context getEglContext() {
|
||||||
|
return eglBase != null ? eglBase.getEglBaseContext() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createPeerConnectionAndAnswer(String offerSdp, String controllerId) {
|
public void createPeerConnectionAndAnswer(String offerSdp, String controllerId) {
|
||||||
|
if (peerConnection != null) {
|
||||||
|
Log.d(TAG, "Closing previous peer connection");
|
||||||
|
closeCurrentConnection();
|
||||||
|
}
|
||||||
this.currentControllerId = controllerId;
|
this.currentControllerId = controllerId;
|
||||||
|
|
||||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(new ArrayList<>());
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||||
|
// 建议:如果你有 TURN 服务器,请务必在此处添加,例如:
|
||||||
|
// iceServers.add(PeerConnection.IceServer.builder("turn:your_turn_server").setUsername("user").setPassword("pass").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("turn:175.178.213.60:3478").setUsername("fanhuitong").setPassword("Fan19961207..").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer());
|
||||||
|
|
||||||
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
|
||||||
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
||||||
|
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||||
|
config.iceCandidatePoolSize = 10;
|
||||||
|
// 建议启用,以便在复杂网络下有更好的表现
|
||||||
|
config.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED;
|
||||||
|
|
||||||
PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() {
|
PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -108,18 +161,18 @@ public class WebRtcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddStream(MediaStream stream) {}
|
public void onAddStream(MediaStream stream) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRemoveStream(MediaStream stream) {}
|
public void onRemoveStream(MediaStream stream) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
||||||
|
|
||||||
if (videoTrack != null) {
|
if (videoTrack != null) {
|
||||||
MediaStream stream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
|
peerConnection.addTrack(videoTrack, Collections.singletonList(STREAM_ID));
|
||||||
stream.addTrack(videoTrack);
|
|
||||||
peerConnection.addStream(stream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建本地 DataChannel
|
// 创建本地 DataChannel
|
||||||
@@ -132,7 +185,8 @@ public class WebRtcClient {
|
|||||||
SessionDescription remoteSdp = new SessionDescription(SessionDescription.Type.OFFER, offerSdp);
|
SessionDescription remoteSdp = new SessionDescription(SessionDescription.Type.OFFER, offerSdp);
|
||||||
peerConnection.setRemoteDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() {
|
peerConnection.setRemoteDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCreateSuccess(SessionDescription sdp) {}
|
public void onCreateSuccess(SessionDescription sdp) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetSuccess() {
|
public void onSetSuccess() {
|
||||||
@@ -143,31 +197,47 @@ public class WebRtcClient {
|
|||||||
public void onCreateSuccess(SessionDescription sdp) {
|
public void onCreateSuccess(SessionDescription sdp) {
|
||||||
peerConnection.setLocalDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() {
|
peerConnection.setLocalDescription(new AppSdpObserver(new AppSdpObserver.SdpEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCreateSuccess(SessionDescription s) {}
|
public void onCreateSuccess(SessionDescription s) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetSuccess() {
|
public void onSetSuccess() {
|
||||||
sendAnswer(sdp.description, controllerId);
|
// 尝试通过 SDP Munging 提高码率和优化编码
|
||||||
|
String modifiedSdp = optimizeSdp(sdp.description);
|
||||||
|
sendAnswer(modifiedSdp, controllerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateFailure(String error) {}
|
public void onCreateFailure(String error) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetFailure(String error) {}
|
public void onSetFailure(String error) {
|
||||||
|
}
|
||||||
}), sdp);
|
}), sdp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetSuccess() {}
|
public void onSetSuccess() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateFailure(String error) {}
|
public void onCreateFailure(String error) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetFailure(String error) {}
|
public void onSetFailure(String error) {
|
||||||
|
}
|
||||||
}), constraints);
|
}), constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateFailure(String error) {}
|
public void onCreateFailure(String error) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSetFailure(String error) {}
|
public void onSetFailure(String error) {
|
||||||
|
}
|
||||||
}), remoteSdp);
|
}), remoteSdp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,23 +256,35 @@ public class WebRtcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
closeCurrentConnection();
|
||||||
|
if (peerConnectionFactory != null) {
|
||||||
|
peerConnectionFactory.dispose();
|
||||||
|
peerConnectionFactory = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeCurrentConnection() {
|
||||||
if (dataChannel != null) {
|
if (dataChannel != null) {
|
||||||
|
try {
|
||||||
|
dataChannel.unregisterObserver();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
dataChannel.close();
|
dataChannel.close();
|
||||||
dataChannel.dispose();
|
dataChannel.dispose();
|
||||||
|
dataChannel = null;
|
||||||
}
|
}
|
||||||
if (peerConnection != null) {
|
if (peerConnection != null) {
|
||||||
peerConnection.close();
|
peerConnection.close();
|
||||||
peerConnection.dispose();
|
peerConnection.dispose();
|
||||||
}
|
peerConnection = null;
|
||||||
if (peerConnectionFactory != null) {
|
|
||||||
peerConnectionFactory.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDataChannel(DataChannel dc) {
|
private void handleDataChannel(DataChannel dc) {
|
||||||
dc.registerObserver(new DataChannel.Observer() {
|
dc.registerObserver(new DataChannel.Observer() {
|
||||||
@Override
|
@Override
|
||||||
public void onBufferedAmountChange(long previousAmount) {}
|
public void onBufferedAmountChange(long previousAmount) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStateChange() {
|
public void onStateChange() {
|
||||||
@@ -222,6 +304,44 @@ public class WebRtcClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String optimizeSdp(String sdp) {
|
||||||
|
String[] lines = sdp.split("\r\n");
|
||||||
|
StringBuilder newSdp = new StringBuilder();
|
||||||
|
|
||||||
|
// 优先使用 H264
|
||||||
|
String h264Payload = null;
|
||||||
|
|
||||||
|
// 首先找到 H264 的 payload type
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.startsWith("a=rtpmap:") && line.contains("H264/90000")) {
|
||||||
|
h264Payload = line.split(":")[1].split(" ")[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.startsWith("m=video") && h264Payload != null) {
|
||||||
|
// 将 H264 payload 移到最前面以优先使用硬件加速编码
|
||||||
|
String[] parts = line.split(" ");
|
||||||
|
StringBuilder mLine = new StringBuilder(parts[0] + " " + parts[1] + " " + parts[2]);
|
||||||
|
mLine.append(" ").append(h264Payload);
|
||||||
|
for (int i = 3; i < parts.length; i++) {
|
||||||
|
if (!parts[i].equals(h264Payload)) {
|
||||||
|
mLine.append(" ").append(parts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSdp.append(mLine).append("\r\n");
|
||||||
|
} else if (h264Payload != null && line.startsWith("a=fmtp:" + h264Payload)) {
|
||||||
|
// 为 H264 添加起步码率和最大码率限制 (单位: kbps)
|
||||||
|
// 提高起步码率能显著改善初始画质
|
||||||
|
newSdp.append(line).append(";x-google-start-bitrate=5000;x-google-max-bitrate=50000;x-google-min-bitrate=2000\r\n");
|
||||||
|
} else {
|
||||||
|
newSdp.append(line).append("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSdp.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void sendAnswer(String sdp, String controllerId) {
|
private void sendAnswer(String sdp, String controllerId) {
|
||||||
SignalMessage msg = new SignalMessage();
|
SignalMessage msg = new SignalMessage();
|
||||||
msg.setType("ANSWER");
|
msg.setType("ANSWER");
|
||||||
|
|||||||
@@ -8,60 +8,68 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
android:text="WebRTC 被控端"
|
android:text="WebRTC 被控端"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:layout_marginBottom="24dp"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="信令服务器地址:"
|
android:text="信令服务器地址:"
|
||||||
android:textSize="14sp"/>
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_server_url"
|
android:id="@+id/et_server_url"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textUri"
|
android:layout_marginBottom="16dp"
|
||||||
android:hint="ws://192.168.1.100:8080/ws/signal"
|
android:hint="ws://192.168.100.222:8080/ws/signal"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:inputType="textUri" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="设备ID:"
|
android:text="设备ID:"
|
||||||
android:textSize="14sp"/>
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_device_id"
|
android:id="@+id/et_device_id"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:layout_marginBottom="24dp"
|
||||||
android:hint="设备ID"
|
android:hint="设备ID"
|
||||||
android:layout_marginBottom="24dp"/>
|
android:inputType="text"
|
||||||
|
android:text="981964879" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_status"
|
android:id="@+id/tv_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
android:text="状态: 已停止"
|
android:text="状态: 已停止"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:layout_marginBottom="24dp"/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_start"
|
android:id="@+id/btn_start"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="开始屏幕共享"
|
android:layout_marginBottom="8dp"
|
||||||
android:layout_marginBottom="8dp"/>
|
android:text="开始屏幕共享" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_stop"
|
android:id="@+id/btn_stop"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="停止屏幕共享"
|
android:layout_marginBottom="16dp"
|
||||||
android:enabled="false"/>
|
android:enabled="false"
|
||||||
|
android:text="停止屏幕共享" />
|
||||||
|
|
||||||
|
<org.webrtc.SurfaceViewRenderer
|
||||||
|
android:id="@+id/local_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.1.4' apply false
|
id 'com.android.application' version '8.1.4' apply false
|
||||||
}
|
}
|
||||||
|
apply from: "config.gradle"
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Location of the SDK. This is only used by Gradle.
|
# Location of the SDK. This is only used by Gradle.
|
||||||
# For customization when using a Version Control System, please read the
|
# For customization when using a Version Control System, please read the
|
||||||
# header note.
|
# header note.
|
||||||
#Sat Jul 11 15:36:39 CST 2026
|
#Mon Jul 13 11:51:13 CST 2026
|
||||||
sdk.dir=F\:\\AndroidSDK
|
sdk.dir=D\:\\AndroidSdk\\Windows
|
||||||
|
|||||||
1
WebRTCController/.gitignore
vendored
1
WebRTCController/.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/app/build/
|
/app/build/
|
||||||
/.idea/
|
/.idea/
|
||||||
/.gradle/
|
/.gradle/
|
||||||
|
config.gradle
|
||||||
@@ -2,6 +2,14 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def releaseTime() {
|
||||||
|
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
def appName() {
|
||||||
|
return "WebRTCControlled"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.tt.controller'
|
namespace 'com.tt.controller'
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
@@ -14,17 +22,98 @@ android {
|
|||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
dataBinding true
|
||||||
|
buildConfig true
|
||||||
|
aidl true
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
keypub {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.keypub.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.keypub.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.keypub.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.keypub.keyPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshatch {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.crosshatch.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.crosshatch.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.crosshatch.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.crosshatch.keyPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
zhanxun {
|
||||||
|
storeFile file(rootProject.ext.signingConfigs.zhanxun.storeFile)
|
||||||
|
storePassword rootProject.ext.signingConfigs.zhanxun.storePassword
|
||||||
|
keyAlias rootProject.ext.signingConfigs.zhanxun.keyAlias
|
||||||
|
keyPassword rootProject.ext.signingConfigs.zhanxun.keyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
zhanRuiDebug.initWith(debug)
|
||||||
|
zhanRuiDebug {
|
||||||
|
debuggable true
|
||||||
|
signingConfig signingConfigs.zhanxun
|
||||||
|
}
|
||||||
|
|
||||||
|
zhanRuiRelease.initWith(release)
|
||||||
|
zhanRuiRelease {
|
||||||
|
signingConfig signingConfigs.zhanxun
|
||||||
|
}
|
||||||
|
|
||||||
|
CrosshatchDebug.initWith(debug)
|
||||||
|
CrosshatchDebug {
|
||||||
|
debuggable true
|
||||||
|
signingConfig signingConfigs.crosshatch
|
||||||
|
}
|
||||||
|
|
||||||
|
CrosshatchRelease.initWith(release)
|
||||||
|
CrosshatchRelease {
|
||||||
|
signingConfig signingConfigs.crosshatch
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
versionNameSuffix "_debug"
|
||||||
|
debuggable true
|
||||||
|
minifyEnabled false
|
||||||
|
//Zipalign优化
|
||||||
|
zipAlignEnabled true
|
||||||
|
signingConfig signingConfigs.keypub
|
||||||
|
}
|
||||||
|
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
//Zipalign优化
|
||||||
|
zipAlignEnabled true
|
||||||
|
//前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
//签名
|
||||||
|
signingConfig signingConfigs.keypub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add application variant configuration here instead
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
def buildType = variant.buildType.name
|
||||||
|
def fileName = ""
|
||||||
|
|
||||||
|
if (buildType.contains("debug")) {
|
||||||
|
fileName = "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk"
|
||||||
|
} else {
|
||||||
|
fileName = "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk"
|
||||||
|
}
|
||||||
|
|
||||||
|
output.outputFileName = fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:sharedUserId="android.uid.system">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -8,13 +9,14 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.MaterialComponents.DayNight">
|
android:theme="@style/Theme.MaterialComponents.DayNight">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="landscape">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.tt.controller;
|
package com.tt.controller;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
@@ -21,8 +23,10 @@ import com.tt.controller.webrtc.WebRtcClient;
|
|||||||
|
|
||||||
import org.webrtc.EglBase;
|
import org.webrtc.EglBase;
|
||||||
import org.webrtc.IceCandidate;
|
import org.webrtc.IceCandidate;
|
||||||
|
import org.webrtc.RendererCommon;
|
||||||
import org.webrtc.SurfaceViewRenderer;
|
import org.webrtc.SurfaceViewRenderer;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
@@ -40,6 +44,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private FrameLayout controlPanel;
|
private FrameLayout controlPanel;
|
||||||
private LinearLayout setupPanel;
|
private LinearLayout setupPanel;
|
||||||
private TextView tvStatusControl;
|
private TextView tvStatusControl;
|
||||||
|
private TextView tvStats;
|
||||||
|
|
||||||
private WebSocketClient wsClient;
|
private WebSocketClient wsClient;
|
||||||
private WebRtcClient webRtcClient;
|
private WebRtcClient webRtcClient;
|
||||||
@@ -48,6 +53,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private String myDeviceId;
|
private String myDeviceId;
|
||||||
private String targetDeviceId;
|
private String targetDeviceId;
|
||||||
|
|
||||||
|
private final Handler statsHandler = new Handler(Looper.getMainLooper());
|
||||||
|
private final Runnable statsRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateStats();
|
||||||
|
statsHandler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -64,9 +78,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
controlPanel = findViewById(R.id.control_panel);
|
controlPanel = findViewById(R.id.control_panel);
|
||||||
setupPanel = findViewById(R.id.setup_panel);
|
setupPanel = findViewById(R.id.setup_panel);
|
||||||
tvStatusControl = findViewById(R.id.tv_status_control);
|
tvStatusControl = findViewById(R.id.tv_status_control);
|
||||||
|
tvStats = findViewById(R.id.tv_stats);
|
||||||
|
|
||||||
// 默认服务器地址
|
// 默认服务器地址
|
||||||
etServerUrl.setText("ws://192.168.1.100:8080/ws/signal");
|
etServerUrl.setText("ws://192.168.100.222:8080/ws/signal");
|
||||||
// 生成设备ID
|
// 生成设备ID
|
||||||
myDeviceId = "controller_" + UUID.randomUUID().toString().substring(0, 8);
|
myDeviceId = "controller_" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
etDeviceId.setText(myDeviceId);
|
etDeviceId.setText(myDeviceId);
|
||||||
@@ -77,6 +92,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// 初始化 EGL 和远端视频渲染
|
// 初始化 EGL 和远端视频渲染
|
||||||
eglBase = EglBase.create();
|
eglBase = EglBase.create();
|
||||||
remoteVideoView.init(eglBase.getEglBaseContext(), null);
|
remoteVideoView.init(eglBase.getEglBaseContext(), null);
|
||||||
|
remoteVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
||||||
|
remoteVideoView.setEnableHardwareScaler(true);
|
||||||
remoteVideoView.setZOrderMediaOverlay(true);
|
remoteVideoView.setZOrderMediaOverlay(true);
|
||||||
|
|
||||||
// 设置触摸事件
|
// 设置触摸事件
|
||||||
@@ -255,6 +272,86 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (tvStatusControl != null) {
|
if (tvStatusControl != null) {
|
||||||
tvStatusControl.setText(connected ? "远程控制中" : "未连接");
|
tvStatusControl.setText(connected ? "远程控制中" : "未连接");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
statsHandler.post(statsRunnable);
|
||||||
|
} else {
|
||||||
|
statsHandler.removeCallbacks(statsRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStats() {
|
||||||
|
if (webRtcClient != null) {
|
||||||
|
webRtcClient.getStats(report -> {
|
||||||
|
long width = 0, height = 0, fps = 0, delay = 0;
|
||||||
|
String codecName = "-";
|
||||||
|
double avgDecodeTimeMs = 0;
|
||||||
|
|
||||||
|
for (org.webrtc.RTCStats stats : report.getStatsMap().values()) {
|
||||||
|
if ("inbound-rtp".equals(stats.getType()) && "video".equals(stats.getMembers().get("kind"))) {
|
||||||
|
// 分辨率
|
||||||
|
Object w = stats.getMembers().get("frameWidth");
|
||||||
|
if (w instanceof Number) width = ((Number) w).longValue();
|
||||||
|
Object h = stats.getMembers().get("frameHeight");
|
||||||
|
if (h instanceof Number) height = ((Number) h).longValue();
|
||||||
|
|
||||||
|
// 帧率
|
||||||
|
Object f = stats.getMembers().get("framesPerSecond");
|
||||||
|
if (f instanceof Number) fps = ((Number) f).longValue();
|
||||||
|
|
||||||
|
// 解码耗时 (平均)
|
||||||
|
Object totalTime = stats.getMembers().get("totalDecodeTime");
|
||||||
|
Object frames = stats.getMembers().get("framesDecoded");
|
||||||
|
if (totalTime instanceof Number && frames instanceof Number) {
|
||||||
|
double t = ((Number) totalTime).doubleValue();
|
||||||
|
long c = ((Number) frames).longValue();
|
||||||
|
if (c > 0) {
|
||||||
|
avgDecodeTimeMs = (t * 1000.0) / c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码格式
|
||||||
|
Object codecId = stats.getMembers().get("codecId");
|
||||||
|
if (codecId instanceof String) {
|
||||||
|
org.webrtc.RTCStats codecStats = report.getStatsMap().get(codecId);
|
||||||
|
if (codecStats != null) {
|
||||||
|
Object mime = codecStats.getMembers().get("mimeType");
|
||||||
|
if (mime instanceof String) {
|
||||||
|
codecName = (String) mime;
|
||||||
|
if (codecName.startsWith("video/")) {
|
||||||
|
codecName = codecName.substring(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码器实现
|
||||||
|
Object decoder = stats.getMembers().get("decoderImplementation");
|
||||||
|
if (decoder instanceof String) {
|
||||||
|
String d = (String) decoder;
|
||||||
|
codecName = codecName.equals("-") ? d : codecName + " (" + d + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("candidate-pair".equals(stats.getType())) {
|
||||||
|
Object nominated = stats.getMembers().get("nominated");
|
||||||
|
if (Boolean.TRUE.equals(nominated)) {
|
||||||
|
Object rtt = stats.getMembers().get("currentRoundTripTime");
|
||||||
|
if (rtt instanceof Number) {
|
||||||
|
delay = (long) (((Number) rtt).doubleValue() * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String statsText = String.format(Locale.getDefault(),
|
||||||
|
"分辨率: %dx%d 帧率: %d 延迟: %dms\n解码格式: %s 平均解码耗时: %.1fms",
|
||||||
|
width, height, fps, delay, codecName, avgDecodeTimeMs);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (tvStats != null) {
|
||||||
|
tvStats.setText(statsText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class PeerObserver implements PeerConnection.Observer {
|
|||||||
void onDataChannel(DataChannel dataChannel);
|
void onDataChannel(DataChannel dataChannel);
|
||||||
void onAddStream(MediaStream stream);
|
void onAddStream(MediaStream stream);
|
||||||
void onRemoveStream(MediaStream stream);
|
void onRemoveStream(MediaStream stream);
|
||||||
|
void onTrack(RtpReceiver receiver, MediaStream[] streams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PeerObserver(PeerEventListener listener) {
|
public PeerObserver(PeerEventListener listener) {
|
||||||
@@ -73,5 +74,8 @@ public class PeerObserver implements PeerConnection.Observer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddTrack(RtpReceiver receiver, MediaStream[] streams) {}
|
public void onAddTrack(RtpReceiver receiver, MediaStream[] streams) {
|
||||||
|
android.util.Log.d(TAG, "Track added: " + receiver.id());
|
||||||
|
if (listener != null) listener.onTrack(receiver, streams);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,27 @@ import com.tt.controller.signaling.SignalMessage;
|
|||||||
import com.tt.controller.signaling.WebSocketClient;
|
import com.tt.controller.signaling.WebSocketClient;
|
||||||
|
|
||||||
import org.webrtc.DataChannel;
|
import org.webrtc.DataChannel;
|
||||||
|
import org.webrtc.DefaultVideoDecoderFactory;
|
||||||
|
import org.webrtc.DefaultVideoEncoderFactory;
|
||||||
import org.webrtc.EglBase;
|
import org.webrtc.EglBase;
|
||||||
import org.webrtc.IceCandidate;
|
import org.webrtc.IceCandidate;
|
||||||
import org.webrtc.MediaConstraints;
|
import org.webrtc.MediaConstraints;
|
||||||
import org.webrtc.MediaStream;
|
import org.webrtc.MediaStream;
|
||||||
|
import org.webrtc.MediaStreamTrack;
|
||||||
import org.webrtc.PeerConnection;
|
import org.webrtc.PeerConnection;
|
||||||
import org.webrtc.PeerConnectionFactory;
|
import org.webrtc.PeerConnectionFactory;
|
||||||
|
import org.webrtc.RtpReceiver;
|
||||||
|
import org.webrtc.RtpTransceiver;
|
||||||
import org.webrtc.SessionDescription;
|
import org.webrtc.SessionDescription;
|
||||||
import org.webrtc.SurfaceViewRenderer;
|
import org.webrtc.SurfaceViewRenderer;
|
||||||
|
import org.webrtc.VideoDecoderFactory;
|
||||||
|
import org.webrtc.VideoEncoderFactory;
|
||||||
import org.webrtc.VideoTrack;
|
import org.webrtc.VideoTrack;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class WebRtcClient {
|
public class WebRtcClient {
|
||||||
|
|
||||||
@@ -38,7 +46,8 @@ public class WebRtcClient {
|
|||||||
private DataChannel dataChannel;
|
private DataChannel dataChannel;
|
||||||
private EglBase eglBase;
|
private EglBase eglBase;
|
||||||
private String currentControlledId;
|
private String currentControlledId;
|
||||||
private VideoRenderer videoRenderer;
|
private SurfaceViewRenderer remoteSurfaceView;
|
||||||
|
private VideoTrack remoteVideoTrack;
|
||||||
|
|
||||||
public interface ConnectionListener {
|
public interface ConnectionListener {
|
||||||
void onConnectionEstablished();
|
void onConnectionEstablished();
|
||||||
@@ -66,16 +75,36 @@ public class WebRtcClient {
|
|||||||
PeerConnectionFactory.initialize(initOptions);
|
PeerConnectionFactory.initialize(initOptions);
|
||||||
|
|
||||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||||
|
|
||||||
|
// 创建视频编解码工厂,以支持硬件加速和各种格式
|
||||||
|
VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(
|
||||||
|
eglBase.getEglBaseContext(), true, true);
|
||||||
|
VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
|
||||||
|
|
||||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
.setOptions(options)
|
.setOptions(options)
|
||||||
|
.setVideoEncoderFactory(encoderFactory)
|
||||||
|
.setVideoDecoderFactory(decoderFactory)
|
||||||
.createPeerConnectionFactory();
|
.createPeerConnectionFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createOffer(String controlledId, SurfaceViewRenderer remoteRenderer) {
|
public void createOffer(String controlledId, SurfaceViewRenderer remoteRenderer) {
|
||||||
this.currentControlledId = controlledId;
|
this.currentControlledId = controlledId;
|
||||||
|
|
||||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(new ArrayList<>());
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||||
|
// 添加 STUN 和 TURN 服务器
|
||||||
|
// 注意:如果是 TURN 服务器,必须使用 turn: 前缀
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("turn:175.178.213.60:3478").setUsername("fanhuitong").setPassword("Fan19961207..").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
||||||
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer());
|
||||||
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
|
||||||
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
||||||
|
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||||
|
config.iceCandidatePoolSize = 10;
|
||||||
|
|
||||||
PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() {
|
PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -102,30 +131,41 @@ public class WebRtcClient {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddStream(MediaStream stream) {
|
public void onAddStream(MediaStream stream) {
|
||||||
|
Log.d(TAG, "onAddStream: " + stream.getId());
|
||||||
|
// Unified Plan 下建议使用 onTrack,但为了兼容性也可以处理
|
||||||
if (!stream.videoTracks.isEmpty()) {
|
if (!stream.videoTracks.isEmpty()) {
|
||||||
VideoTrack videoTrack = stream.videoTracks.get(0);
|
VideoTrack videoTrack = stream.videoTracks.get(0);
|
||||||
if (videoRenderer != null) {
|
setupRemoteVideoTrack(videoTrack);
|
||||||
videoTrack.addSink(videoRenderer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRemoveStream(MediaStream stream) {}
|
public void onRemoveStream(MediaStream stream) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTrack(RtpReceiver receiver, MediaStream[] streams) {
|
||||||
|
MediaStreamTrack track = receiver.track();
|
||||||
|
if (track instanceof VideoTrack) {
|
||||||
|
Log.d(TAG, "onTrack (video)");
|
||||||
|
setupRemoteVideoTrack((VideoTrack) track);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
||||||
|
|
||||||
|
// 使用 Unified Plan 方式请求视频流 (仅接收)
|
||||||
|
peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO,
|
||||||
|
new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY));
|
||||||
|
|
||||||
// 创建 DataChannel
|
// 创建 DataChannel
|
||||||
DataChannel.Init dcInit = new DataChannel.Init();
|
DataChannel.Init dcInit = new DataChannel.Init();
|
||||||
dcInit.ordered = true;
|
dcInit.ordered = true;
|
||||||
dataChannel = peerConnection.createDataChannel(DATA_CHANNEL_LABEL, dcInit);
|
dataChannel = peerConnection.createDataChannel(DATA_CHANNEL_LABEL, dcInit);
|
||||||
handleDataChannel(dataChannel);
|
handleDataChannel(dataChannel);
|
||||||
|
|
||||||
// 初始化远端视频渲染器
|
// 保存远端视频渲染器
|
||||||
if (remoteRenderer != null) {
|
this.remoteSurfaceView = remoteRenderer;
|
||||||
videoRenderer = new VideoRenderer(remoteRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Offer
|
// 创建 Offer
|
||||||
MediaConstraints constraints = new MediaConstraints();
|
MediaConstraints constraints = new MediaConstraints();
|
||||||
@@ -193,6 +233,9 @@ public class WebRtcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
if (remoteVideoTrack != null && remoteSurfaceView != null) {
|
||||||
|
remoteVideoTrack.removeSink(remoteSurfaceView);
|
||||||
|
}
|
||||||
if (dataChannel != null) {
|
if (dataChannel != null) {
|
||||||
dataChannel.close();
|
dataChannel.close();
|
||||||
dataChannel.dispose();
|
dataChannel.dispose();
|
||||||
@@ -206,6 +249,15 @@ public class WebRtcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupRemoteVideoTrack(VideoTrack videoTrack) {
|
||||||
|
if (remoteSurfaceView != null) {
|
||||||
|
this.remoteVideoTrack = videoTrack;
|
||||||
|
remoteVideoTrack.setEnabled(true);
|
||||||
|
remoteVideoTrack.addSink(remoteSurfaceView);
|
||||||
|
Log.d(TAG, "Remote video track attached to renderer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleDataChannel(DataChannel dc) {
|
private void handleDataChannel(DataChannel dc) {
|
||||||
dc.registerObserver(new DataChannel.Observer() {
|
dc.registerObserver(new DataChannel.Observer() {
|
||||||
@Override
|
@Override
|
||||||
@@ -255,4 +307,10 @@ public class WebRtcClient {
|
|||||||
|
|
||||||
wsClient.sendMessage(msg);
|
wsClient.sendMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getStats(org.webrtc.RTCStatsCollectorCallback callback) {
|
||||||
|
if (peerConnection != null) {
|
||||||
|
peerConnection.getStats(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,82 +8,83 @@
|
|||||||
android:id="@+id/setup_panel"
|
android:id="@+id/setup_panel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="24dp"
|
android:padding="24dp">
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
android:text="WebRTC 控制端"
|
android:text="WebRTC 控制端"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:layout_marginBottom="24dp"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="信令服务器地址:"
|
android:text="信令服务器地址:"
|
||||||
android:textSize="14sp"/>
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_server_url"
|
android:id="@+id/et_server_url"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textUri"
|
android:layout_marginBottom="16dp"
|
||||||
android:hint="ws://192.168.1.100:8080/ws/signal"
|
android:hint="ws://192.168.100.222:8080/ws/signal"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:inputType="textUri" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="本机设备ID:"
|
android:text="本机设备ID:"
|
||||||
android:textSize="14sp"/>
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_device_id"
|
android:id="@+id/et_device_id"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:layout_marginBottom="16dp"
|
||||||
android:hint="设备ID"
|
android:hint="设备ID"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:inputType="text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="目标被控设备ID:"
|
android:text="目标被控设备ID:"
|
||||||
android:textSize="14sp"/>
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_target_device_id"
|
android:id="@+id/et_target_device_id"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:layout_marginBottom="24dp"
|
||||||
android:hint="被控端设备ID"
|
android:hint="被控端设备ID"
|
||||||
android:layout_marginBottom="24dp"/>
|
android:inputType="text"
|
||||||
|
android:text="981964879" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_status"
|
android:id="@+id/tv_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
android:text="状态: 已停止"
|
android:text="状态: 已停止"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:layout_marginBottom="24dp"/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_connect"
|
android:id="@+id/btn_connect"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="连接被控设备"
|
android:layout_marginBottom="8dp"
|
||||||
android:layout_marginBottom="8dp"/>
|
android:text="连接被控设备" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_disconnect"
|
android:id="@+id/btn_disconnect"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="断开连接"
|
android:enabled="false"
|
||||||
android:enabled="false"/>
|
android:text="断开连接" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -98,26 +99,41 @@
|
|||||||
<org.webrtc.SurfaceViewRenderer
|
<org.webrtc.SurfaceViewRenderer
|
||||||
android:id="@+id/remote_video_view"
|
android:id="@+id/remote_video_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<!-- 触摸控制层(覆盖在视频上方) -->
|
<!-- 触摸控制层(覆盖在视频上方) -->
|
||||||
<com.tt.controller.view.RemoteTouchView
|
<com.tt.controller.view.RemoteTouchView
|
||||||
android:id="@+id/touch_overlay"
|
android:id="@+id/touch_overlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@android:color/transparent"/>
|
android:background="@android:color/transparent" />
|
||||||
|
|
||||||
<!-- 顶部状态栏 -->
|
<!-- 顶部状态栏 -->
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tv_status_control"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top|start"
|
android:layout_gravity="top|start"
|
||||||
android:padding="8dp"
|
|
||||||
android:background="#80000000"
|
android:background="#80000000"
|
||||||
android:textColor="@android:color/white"
|
android:orientation="vertical"
|
||||||
android:text="远程控制中"
|
android:padding="8dp">
|
||||||
android:textSize="14sp"/>
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_status_control"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="远程控制中"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_stats"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="分辨率: - 帧率: - 延迟: -"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.1.4' apply false
|
id 'com.android.application' version '8.1.4' apply false
|
||||||
}
|
}
|
||||||
|
apply from: "config.gradle"
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Location of the SDK. This is only used by Gradle.
|
# Location of the SDK. This is only used by Gradle.
|
||||||
# For customization when using a Version Control System, please read the
|
# For customization when using a Version Control System, please read the
|
||||||
# header note.
|
# header note.
|
||||||
#Sat Jul 11 15:36:50 CST 2026
|
#Mon Jul 13 11:51:30 CST 2026
|
||||||
sdk.dir=F\:\\AndroidSDK
|
sdk.dir=D\:\\AndroidSdk\\Windows
|
||||||
|
|||||||
Reference in New Issue
Block a user