build: 更新项目至studio panda
This commit is contained in:
@@ -29,6 +29,10 @@ android {
|
|||||||
enabled true
|
enabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewBinding {
|
||||||
|
enabled true
|
||||||
|
}
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
annotationProcessorOptions {
|
annotationProcessorOptions {
|
||||||
arguments = [AROUTER_MODULE_NAME: project.getName()]
|
arguments = [AROUTER_MODULE_NAME: project.getName()]
|
||||||
@@ -101,7 +105,7 @@ dependencies {
|
|||||||
implementation 'com.jeremyliao:live-event-bus-x:1.7.3'
|
implementation 'com.jeremyliao:live-event-bus-x:1.7.3'
|
||||||
|
|
||||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||||
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
|
// implementation 'org.java-websocket:Java-WebSocket:1.5.3'
|
||||||
|
|
||||||
//MMKV
|
//MMKV
|
||||||
implementation 'com.tencent:mmkv-static:1.2.14'
|
implementation 'com.tencent:mmkv-static:1.2.14'
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import android.os.Bundle;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -23,7 +25,11 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
import com.ttstd.remoteservice.R;
|
import com.ttstd.remoteservice.R;
|
||||||
|
import com.ttstd.remoteservice.network.WebSocketCallback;
|
||||||
|
import com.ttstd.remoteservice.network.WebSocketManager;
|
||||||
import com.ttstd.remoteservice.service.ScreenCaptureService2;
|
import com.ttstd.remoteservice.service.ScreenCaptureService2;
|
||||||
|
|
||||||
import org.webrtc.CapturerObserver;
|
import org.webrtc.CapturerObserver;
|
||||||
@@ -46,7 +52,9 @@ import org.webrtc.VideoTrack;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
import okio.ByteString;
|
||||||
|
|
||||||
|
public class WebRTCScreenCaptureActivity extends AppCompatActivity implements WebSocketCallback {
|
||||||
private static final String TAG = "WebRTCScreenCaptureActivity";
|
private static final String TAG = "WebRTCScreenCaptureActivity";
|
||||||
// 请求码
|
// 请求码
|
||||||
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001;
|
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001;
|
||||||
@@ -71,7 +79,8 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
private int screenDpi;
|
private int screenDpi;
|
||||||
|
|
||||||
// UI控件
|
// UI控件
|
||||||
private Button btnStartPush;
|
private TextView tvStatus;
|
||||||
|
private Button btConnect, btnStartPush;
|
||||||
private SurfaceViewRenderer localRender; // 本地预览(可选)
|
private SurfaceViewRenderer localRender; // 本地预览(可选)
|
||||||
|
|
||||||
private TextureView textureView;
|
private TextureView textureView;
|
||||||
@@ -82,16 +91,27 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
private String remoteSdp = ""; // 替换为远端实际SDP
|
private String remoteSdp = ""; // 替换为远端实际SDP
|
||||||
private List<IceCandidate> remoteIceCandidates = new ArrayList<>();
|
private List<IceCandidate> remoteIceCandidates = new ArrayList<>();
|
||||||
|
|
||||||
|
private WebSocketManager webSocketManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_webrtc_screen_capture);
|
setContentView(R.layout.activity_webrtc_screen_capture);
|
||||||
|
|
||||||
// 初始化控件
|
// 初始化控件
|
||||||
|
tvStatus = findViewById(R.id.tv_status);
|
||||||
|
btConnect = findViewById(R.id.bt_connect);
|
||||||
btnStartPush = findViewById(R.id.btn_start_push);
|
btnStartPush = findViewById(R.id.btn_start_push);
|
||||||
localRender = findViewById(R.id.local_render);
|
localRender = findViewById(R.id.local_render);
|
||||||
textureView = findViewById(R.id.textureView);
|
textureView = findViewById(R.id.textureView);
|
||||||
|
|
||||||
|
btConnect.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
initWebSocket();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
||||||
@@ -173,6 +193,15 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
|
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initWebSocket() {
|
||||||
|
Log.e(TAG, "initWebSocket: ");
|
||||||
|
// 初始化 WebSocket 管理器
|
||||||
|
webSocketManager = WebSocketManager.getInstance();
|
||||||
|
// String wsUrl = "ws://175.178.213.60:2310/signaling/981964879";
|
||||||
|
String wsUrl = "ws://192.168.100.111:2310/signaling/981964879";
|
||||||
|
webSocketManager.init(wsUrl, this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化WebRTC核心组件
|
* 初始化WebRTC核心组件
|
||||||
*/
|
*/
|
||||||
@@ -348,8 +377,8 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// 添加用户指定的 STUN 服务器
|
// 添加用户指定的 STUN 服务器
|
||||||
PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478")
|
PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478")
|
||||||
.setUsername("tt")
|
.setUsername("tt")
|
||||||
.setPassword("tongtong")
|
.setPassword("tongtong")
|
||||||
.createIceServer();
|
.createIceServer();
|
||||||
iceServers.add(stunServer);
|
iceServers.add(stunServer);
|
||||||
|
|
||||||
@@ -368,8 +397,139 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
return iceServers;
|
return iceServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送SDP给远端信令服务器
|
||||||
|
*/
|
||||||
private void sendSdpToRemote(SessionDescription sdp) {
|
private void sendSdpToRemote(SessionDescription sdp) {
|
||||||
// TODO: 实现信令发送逻辑,例如通过WebSocket发送SDP字符串
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
jsonObject.addProperty("msg_type", 1);
|
||||||
|
|
||||||
|
JsonObject sdpJson = new JsonObject();
|
||||||
|
sdpJson.addProperty("type", sdp.type.canonicalForm()); // "offer" 或 "answer"
|
||||||
|
sdpJson.addProperty("sdp", sdp.description);
|
||||||
|
// TODO: 2026/3/11 之后改为唯一标识码
|
||||||
|
sdpJson.addProperty("fromUser", "981964879"); // 标识发送方
|
||||||
|
sdpJson.addProperty("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
jsonObject.add("content", sdpJson);
|
||||||
|
String json = jsonObject.toString();
|
||||||
|
|
||||||
|
Log.d(TAG, "发送SDP到信令服务器: ");
|
||||||
|
Log.e(TAG, "sendSdpToRemote: SDP内容: " + json);
|
||||||
|
|
||||||
|
webSocketManager.sendMessage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送ICE Candidate给远端信令服务器
|
||||||
|
*/
|
||||||
|
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
|
||||||
|
Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp);
|
||||||
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
jsonObject.addProperty("msg_type", 2);
|
||||||
|
|
||||||
|
JsonObject iceJson = new JsonObject();
|
||||||
|
iceJson.addProperty("sdpMid", iceCandidate.sdpMid);
|
||||||
|
iceJson.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
|
||||||
|
iceJson.addProperty("sdp", iceCandidate.sdp);
|
||||||
|
|
||||||
|
// TODO: 2026/3/11 之后改为唯一标识码
|
||||||
|
iceJson.addProperty("fromUser", "981964879");
|
||||||
|
iceJson.addProperty("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
jsonObject.add("content", iceJson);
|
||||||
|
|
||||||
|
String json = jsonObject.toString();
|
||||||
|
|
||||||
|
Log.d(TAG, "发送ICE Candidate到信令服务器: ");
|
||||||
|
Log.e(TAG, "sendIceCandidateToRemote: ICE内容: " + json);
|
||||||
|
webSocketManager.sendMessage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理信令服务器响应(接收远端的Answer SDP)
|
||||||
|
*/
|
||||||
|
private void handleSignalingResponse(String response) {
|
||||||
|
JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject();
|
||||||
|
;
|
||||||
|
|
||||||
|
// 检查是否包含远端的Answer SDP
|
||||||
|
if (jsonResponse.has("type") && "answer".equals(jsonResponse.get("type").getAsString())) {
|
||||||
|
String remoteSdp = jsonResponse.get("sdp").getAsString();
|
||||||
|
SessionDescription remoteSessionDescription =
|
||||||
|
new SessionDescription(SessionDescription.Type.ANSWER, remoteSdp);
|
||||||
|
|
||||||
|
// 设置远端SDP
|
||||||
|
peerConnection.setRemoteDescription(new SimpleSdpObserver(), remoteSessionDescription);
|
||||||
|
Log.d(TAG, "已设置远端Answer SDP");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含远端的ICE Candidate
|
||||||
|
if (jsonResponse.has("candidate")) {
|
||||||
|
String sdpMid = jsonResponse.get("sdpMid").getAsString();
|
||||||
|
int sdpMLineIndex = jsonResponse.get("sdpMLineIndex").getAsInt();
|
||||||
|
String candidate = jsonResponse.get("candidate").getAsString();
|
||||||
|
|
||||||
|
IceCandidate remoteIceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate);
|
||||||
|
peerConnection.addIceCandidate(remoteIceCandidate);
|
||||||
|
Log.d(TAG, "已添加远端ICE Candidate");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnected() {
|
||||||
|
Log.e(TAG, "onConnected: ");
|
||||||
|
tvStatus.setText("websocket已连接");
|
||||||
|
btConnect.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(String reason) {
|
||||||
|
Log.e(TAG, "onDisconnected: " + reason);
|
||||||
|
tvStatus.setText("websocket已断开:" + reason);
|
||||||
|
btConnect.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String message) {
|
||||||
|
Log.e(TAG, "onMessage: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(ByteString bytes) {
|
||||||
|
Log.e(TAG, "onMessage: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String error) {
|
||||||
|
Log.e(TAG, "onError: " + error);
|
||||||
|
tvStatus.setText("websocket错误:" + error);
|
||||||
|
btConnect.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化的SdpObserver实现
|
||||||
|
*/
|
||||||
|
private static class SimpleSdpObserver implements SdpObserver {
|
||||||
|
@Override
|
||||||
|
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetSuccess() {
|
||||||
|
Log.d(TAG, "SDP设置成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateFailure(String error) {
|
||||||
|
Log.e(TAG, "创建SDP失败: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetFailure(String error) {
|
||||||
|
Log.e(TAG, "设置SDP失败: " + error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -476,15 +636,6 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送ICE候选到远端(示例:实际需通过WebSocket/HTTP信令服务器)
|
|
||||||
*/
|
|
||||||
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
|
|
||||||
// 这里仅做日志打印,实际需替换为信令发送逻辑
|
|
||||||
Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp);
|
|
||||||
// 示例:将iceCandidate转为JSON发送到远端服务器
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理权限请求结果
|
* 处理权限请求结果
|
||||||
*/
|
*/
|
||||||
@@ -606,6 +757,10 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity {
|
|||||||
// rootEglBase.release();
|
// rootEglBase.release();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (webSocketManager != null) {
|
||||||
|
webSocketManager.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
// 重置按钮
|
// 重置按钮
|
||||||
btnStartPush.setText("开始推流");
|
btnStartPush.setText("开始推流");
|
||||||
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
|||||||
private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897;
|
private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897;
|
||||||
private MediaProjectionManager mMediaProjectionManager;
|
private MediaProjectionManager mMediaProjectionManager;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setNightMode() {
|
public boolean setNightMode() {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ttstd.remoteservice.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 配置类
|
||||||
|
* 用于配置WebSocket连接的各种参数
|
||||||
|
*/
|
||||||
|
public class WebSocketConfig {
|
||||||
|
public String wsUrl; // WebSocket服务器地址
|
||||||
|
public static final long heartbeatInterval = 30000; // 心跳间隔,默认10秒
|
||||||
|
public static final long reconnectInterval = 5000; // 重连间隔,默认5秒
|
||||||
|
public static final int maxReconnectAttempts = 5; // 最大重连次数
|
||||||
|
public static final boolean autoReconnect = true; // 是否自动重连
|
||||||
|
public static final boolean needHeartbeat = true; // 是否需要心跳检测
|
||||||
|
public static final long connectTimeout = 10; // 连接超时时间(秒)
|
||||||
|
public static final long readTimeout = 10; // 读取超时时间(秒)
|
||||||
|
public static final long writeTimeout = 10; // 写入超时时间(秒)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.ttstd.remoteservice.network;
|
||||||
|
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 回调接口
|
||||||
|
* 所有回调都在主线程执行,方便更新 UI
|
||||||
|
*/
|
||||||
|
public interface WebSocketCallback {
|
||||||
|
/**
|
||||||
|
* 连接成功
|
||||||
|
*/
|
||||||
|
void onConnected();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接断开
|
||||||
|
*
|
||||||
|
* @param reason 断开原因
|
||||||
|
*/
|
||||||
|
void onDisconnected(String reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收到文本消息
|
||||||
|
*
|
||||||
|
* @param message 文本内容
|
||||||
|
*/
|
||||||
|
void onMessage(String message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收到二进制消息
|
||||||
|
*
|
||||||
|
* @param bytes 二进制数据
|
||||||
|
*/
|
||||||
|
void onMessage(ByteString bytes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接发生错误
|
||||||
|
*
|
||||||
|
* @param error 错误信息
|
||||||
|
*/
|
||||||
|
void onError(String error);
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
package com.ttstd.remoteservice.network;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.ttstd.remoteservice.config.WebSocketConfig;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 管理类(单例模式)
|
||||||
|
* 功能:自动重连、心跳检测、连接状态管理
|
||||||
|
*/
|
||||||
|
public class WebSocketManager {
|
||||||
|
private static final String TAG = "WebSocketManager";
|
||||||
|
private static volatile WebSocketManager instance;
|
||||||
|
|
||||||
|
private OkHttpClient client;
|
||||||
|
private WebSocket webSocket;
|
||||||
|
private WebSocketCallback callback;
|
||||||
|
private String wsUrl;
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
private boolean isConnected = false;
|
||||||
|
private boolean shouldReconnect = true;
|
||||||
|
|
||||||
|
private Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
private Runnable reconnectRunnable;
|
||||||
|
private Runnable heartbeatRunnable;
|
||||||
|
|
||||||
|
// 重连相关
|
||||||
|
private int reconnectAttempts = 0;
|
||||||
|
|
||||||
|
// 消息队列(线程安全)
|
||||||
|
private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
// 连接状态锁
|
||||||
|
private final Object connectionLock = new Object();
|
||||||
|
|
||||||
|
private WebSocketManager() {
|
||||||
|
// 私有构造函数
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebSocketManager getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (WebSocketManager.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new WebSocketManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 WebSocket 连接
|
||||||
|
*/
|
||||||
|
public void init(String url, WebSocketCallback callback) {
|
||||||
|
this.wsUrl = url;
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
|
||||||
|
.pingInterval(10, TimeUnit.SECONDS) // 设置心跳
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立 WebSocket 连接
|
||||||
|
*/
|
||||||
|
private void connect() {
|
||||||
|
if (wsUrl == null) {
|
||||||
|
Log.e(TAG, "WebSocket URL 不能为 null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(wsUrl)) {
|
||||||
|
Log.e(TAG, "WebSocketConfig or URL is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(wsUrl)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
webSocket = client.newWebSocket(request, new InnerWebSocketListener());
|
||||||
|
Log.d(TAG, "Connecting to WebSocket: " + wsUrl);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Connect failed: " + e.getMessage());
|
||||||
|
notifyError("连接失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送文本消息
|
||||||
|
*/
|
||||||
|
public boolean sendMessage(String message) {
|
||||||
|
if (webSocket != null && isConnected) {
|
||||||
|
boolean sent = webSocket.send(message);
|
||||||
|
if (sent) {
|
||||||
|
Log.d(TAG, "Message sent: " + message);
|
||||||
|
} else {
|
||||||
|
// 发送失败,加入队列
|
||||||
|
messageQueue.offer(message);
|
||||||
|
Log.w(TAG, "消息发送失败,已加入队列: " + message);
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
} else {
|
||||||
|
// 未连接,加入队列等待重发
|
||||||
|
messageQueue.offer(message);
|
||||||
|
Log.e(TAG, "WebSocket not connected, cannot send message");
|
||||||
|
notifyError("WebSocket未连接,无法发送消息");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送二进制消息
|
||||||
|
*/
|
||||||
|
public boolean sendMessage(ByteString bytes) {
|
||||||
|
if (webSocket != null && isConnected) {
|
||||||
|
return webSocket.send(bytes);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "WebSocket not connected, cannot send binary message");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 WebSocket 连接
|
||||||
|
*/
|
||||||
|
public void disconnect() {
|
||||||
|
Log.d(TAG, "Disconnecting WebSocket");
|
||||||
|
|
||||||
|
shouldReconnect = false;
|
||||||
|
|
||||||
|
// 停止重连和心跳
|
||||||
|
stopReconnect();
|
||||||
|
stopHeartbeat();
|
||||||
|
|
||||||
|
if (webSocket != null) {
|
||||||
|
webSocket.close(1000, "正常关闭");
|
||||||
|
webSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected = false;
|
||||||
|
Log.d(TAG, "WebSocket 连接已关闭");
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
public boolean isConnected() {
|
||||||
|
return isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始心跳检测
|
||||||
|
*/
|
||||||
|
private void startHeartbeat() {
|
||||||
|
stopHeartbeat();
|
||||||
|
|
||||||
|
heartbeatRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isConnected && webSocket != null) {
|
||||||
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
jsonObject.addProperty("msg_type", 0);
|
||||||
|
jsonObject.addProperty("content", "ping");
|
||||||
|
// 发送心跳消息(可以是空消息或特定协议)
|
||||||
|
boolean sent = webSocket.send(jsonObject.toString());
|
||||||
|
Log.d(TAG, "发送心跳包");
|
||||||
|
if (sent) {
|
||||||
|
// 继续下一次心跳
|
||||||
|
mainHandler.postDelayed(this, WebSocketConfig.heartbeatInterval);
|
||||||
|
} else {
|
||||||
|
// 发送失败,尝试重连
|
||||||
|
handleDisconnect("Heartbeat failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainHandler.postDelayed(heartbeatRunnable, WebSocketConfig.heartbeatInterval);
|
||||||
|
Log.d(TAG, "Heartbeat started");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理断开连接
|
||||||
|
*/
|
||||||
|
private void handleDisconnect(String reason) {
|
||||||
|
Log.d(TAG, "Handle disconnect: " + reason);
|
||||||
|
|
||||||
|
isConnected = false;
|
||||||
|
stopHeartbeat();
|
||||||
|
|
||||||
|
notifyDisconnected(reason);
|
||||||
|
|
||||||
|
// 自动重连逻辑
|
||||||
|
if (WebSocketConfig.autoReconnect && reconnectAttempts < WebSocketConfig.maxReconnectAttempts) {
|
||||||
|
scheduleReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排重连
|
||||||
|
*/
|
||||||
|
private void scheduleReconnect() {
|
||||||
|
if (!shouldReconnect) return;
|
||||||
|
|
||||||
|
reconnectAttempts++;
|
||||||
|
Log.d(TAG, "Scheduling reconnect attempt " + reconnectAttempts + " after " + WebSocketConfig.reconnectInterval + "ms");
|
||||||
|
|
||||||
|
reconnectRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.d(TAG, "尝试重连 WebSocket");
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainHandler.postDelayed(reconnectRunnable, WebSocketConfig.heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止重连
|
||||||
|
*/
|
||||||
|
private void stopReconnect() {
|
||||||
|
if (heartbeatRunnable != null) {
|
||||||
|
mainHandler.removeCallbacks(heartbeatRunnable);
|
||||||
|
heartbeatRunnable = null;
|
||||||
|
}
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳检测
|
||||||
|
*/
|
||||||
|
private void stopHeartbeat() {
|
||||||
|
if (heartbeatRunnable != null) {
|
||||||
|
mainHandler.removeCallbacks(heartbeatRunnable);
|
||||||
|
heartbeatRunnable = null;
|
||||||
|
}
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部WebSocket监听器
|
||||||
|
*/
|
||||||
|
private class InnerWebSocketListener extends WebSocketListener {
|
||||||
|
@Override
|
||||||
|
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
|
||||||
|
Log.d(TAG, "WebSocket connected successfully");
|
||||||
|
|
||||||
|
isConnected = true;
|
||||||
|
reconnectAttempts = 0; // 重置重连计数器
|
||||||
|
|
||||||
|
// 连接成功,发送队列中的积压消息
|
||||||
|
processMessageQueue();
|
||||||
|
|
||||||
|
// 在主线程通知连接成功
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onConnected();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始心跳检测
|
||||||
|
startHeartbeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
|
||||||
|
Log.d(TAG, "Received text message: " + text);
|
||||||
|
|
||||||
|
// 在主线程通知消息接收
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onMessage(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NonNull WebSocket webSocket, @NonNull ByteString bytes) {
|
||||||
|
Log.d(TAG, "Received binary message, size: " + bytes.size());
|
||||||
|
|
||||||
|
// 在主线程通知消息接收
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onMessage(bytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
|
||||||
|
Log.d(TAG, "WebSocket closing: " + code + " - " + reason);
|
||||||
|
webSocket.close(1000, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
|
||||||
|
Log.d(TAG, "WebSocket closed: " + code + " - " + reason);
|
||||||
|
handleDisconnect("Connection closed: " + reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, Response response) {
|
||||||
|
Log.e(TAG, "WebSocket failure: " + t.getMessage());
|
||||||
|
handleDisconnect("Connection failure: " + t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理消息队列(发送所有积压消息)
|
||||||
|
*/
|
||||||
|
private void processMessageQueue() {
|
||||||
|
new Thread(() -> {
|
||||||
|
synchronized (connectionLock) {
|
||||||
|
while (!messageQueue.isEmpty()) {
|
||||||
|
String message = messageQueue.poll();
|
||||||
|
if (message != null && isConnected && webSocket != null) {
|
||||||
|
boolean sent = webSocket.send(message);
|
||||||
|
if (sent) {
|
||||||
|
Log.d(TAG, "队列消息发送成功: " + message);
|
||||||
|
} else {
|
||||||
|
// 发送失败,重新放回队列头部
|
||||||
|
messageQueue.offer(message);
|
||||||
|
Log.e(TAG, "队列消息发送失败,已重新排队: " + message);
|
||||||
|
break; // 暂停发送,避免连续失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加短暂延迟,避免洪水攻击服务器
|
||||||
|
try {
|
||||||
|
Thread.sleep(50);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取队列大小(用于监控)
|
||||||
|
*/
|
||||||
|
public int getQueueSize() {
|
||||||
|
return messageQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空消息队列
|
||||||
|
*/
|
||||||
|
public void clearQueue() {
|
||||||
|
messageQueue.clear();
|
||||||
|
Log.d(TAG, "消息队列已清空");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知错误(主线程)
|
||||||
|
*/
|
||||||
|
private void notifyError(String error) {
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知断开连接(主线程)
|
||||||
|
*/
|
||||||
|
private void notifyDisconnected(String reason) {
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onDisconnected(reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
public void release() {
|
||||||
|
disconnect();
|
||||||
|
if (client != null) {
|
||||||
|
client.dispatcher().executorService().shutdown();
|
||||||
|
}
|
||||||
|
instance = null;
|
||||||
|
Log.d(TAG, "WebSocketManager released");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,41 @@
|
|||||||
package com.ttstd.remoteservice.utils;
|
package com.ttstd.remoteservice.utils;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SystemUtils {
|
public class SystemUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备序列号
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
public static String getSerial() {
|
||||||
|
String serial = "unknow";
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
|
||||||
|
serial = Build.getSerial();
|
||||||
|
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+
|
||||||
|
serial = Build.SERIAL;
|
||||||
|
} else {//8.0-
|
||||||
|
Class<?> c = Class.forName("android.os.SystemProperties");
|
||||||
|
Method get = c.getMethod("get", String.class);
|
||||||
|
serial = (String) get.invoke(c, "ro.serialno");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.e("e", "读取设备序列号异常:" + e.toString());
|
||||||
|
}
|
||||||
|
return serial;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isMainProcessName(Context cxt, int pid) {
|
public static boolean isMainProcessName(Context cxt, int pid) {
|
||||||
String packageName = cxt.getPackageName();
|
String packageName = cxt.getPackageName();
|
||||||
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
|
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
|||||||
@@ -1,26 +1,57 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_height="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical">
|
tools:context=".activity.WebRTCScreenCaptureActivity">
|
||||||
|
|
||||||
<Button
|
<data>
|
||||||
android:id="@+id/btn_start_push"
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:text="开始推流" />
|
android:orientation="vertical">
|
||||||
|
|
||||||
<!-- WebRTC本地预览(显示采集的屏幕内容) -->
|
<TextView
|
||||||
<org.webrtc.SurfaceViewRenderer
|
android:id="@+id/tv_status"
|
||||||
android:id="@+id/local_render"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="100dp"
|
android:layout_gravity="center"
|
||||||
android:layout_weight="1" />
|
android:text="等待连接..."
|
||||||
|
android:textColor="#000000"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<TextureView
|
<Button
|
||||||
android:id="@+id/textureView"
|
android:id="@+id/bt_connect"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="100dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:layout_gravity="center"
|
||||||
|
android:text="连接websocket"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
<Button
|
||||||
|
android:id="@+id/btn_start_push"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="开始推流" />
|
||||||
|
|
||||||
|
<!-- WebRTC本地预览(显示采集的屏幕内容) -->
|
||||||
|
<org.webrtc.SurfaceViewRenderer
|
||||||
|
android:id="@+id/local_render"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/textureView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</layout>
|
||||||
@@ -13,8 +13,8 @@ buildscript {
|
|||||||
maven { url 'https://maven.aliyun.com/repository/google' }
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.6.4'
|
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||||
|
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user