feat: 交换信令
This commit is contained in:
@@ -43,8 +43,6 @@
|
||||
android:name=".service.ScreenCaptureService"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<!-- 指令执行服务 -->
|
||||
<service android:name=".service.ControlService" />
|
||||
<service
|
||||
android:name=".service.ScreenCaptureService2"
|
||||
android:exported="false"
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
package com.ttstd.remoteservice.activity;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -25,470 +19,167 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.ttstd.remoteservice.R;
|
||||
import com.ttstd.remoteservice.network.WebSocketCallback;
|
||||
import com.ttstd.remoteservice.network.WebSocketManager;
|
||||
import com.ttstd.remoteservice.service.ScreenCaptureService2;
|
||||
|
||||
import org.webrtc.CapturerObserver;
|
||||
import org.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.DefaultVideoEncoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SdpObserver;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class WebRTCScreenCaptureActivity extends AppCompatActivity implements WebSocketCallback {
|
||||
private static final String TAG = "WebRTCScreenCaptureActivity";
|
||||
// 请求码
|
||||
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001;
|
||||
private static final int REQUEST_CODE_PERMISSIONS = 1002;
|
||||
|
||||
// WebRTC核心组件
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
private VideoSource videoSource;
|
||||
private VideoTrack videoTrack;
|
||||
private EglBase rootEglBase;
|
||||
private WebSocketManager mWebSocketManager;
|
||||
|
||||
// 屏幕采集相关
|
||||
private MediaProjectionManager mediaProjectionManager;
|
||||
private MediaProjection mediaProjection;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
private ScreenVideoCapturer screenVideoCapturer; // 自定义屏幕采集器
|
||||
|
||||
// 屏幕参数
|
||||
private int screenWidth;
|
||||
private int screenHeight;
|
||||
private int screenDpi;
|
||||
|
||||
// UI控件
|
||||
private TextView tvStatus;
|
||||
private Button btConnect, btnStartPush;
|
||||
private SurfaceViewRenderer localRender; // 本地预览(可选)
|
||||
|
||||
private TextureView textureView;
|
||||
|
||||
private VirtualDisplay mVirtualDisplay;
|
||||
|
||||
// 远端信令信息(示例:实际需从信令服务器获取)
|
||||
private String remoteSdp = ""; // 替换为远端实际SDP
|
||||
private List<IceCandidate> remoteIceCandidates = new ArrayList<>();
|
||||
|
||||
private WebSocketManager webSocketManager;
|
||||
private SurfaceViewRenderer localRender;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_webrtc_screen_capture);
|
||||
|
||||
// 初始化控件
|
||||
tvStatus = findViewById(R.id.tv_status);
|
||||
btConnect = findViewById(R.id.bt_connect);
|
||||
btnStartPush = findViewById(R.id.btn_start_push);
|
||||
localRender = findViewById(R.id.local_render);
|
||||
textureView = findViewById(R.id.textureView);
|
||||
|
||||
btConnect.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
initWebSocket();
|
||||
}
|
||||
});
|
||||
mWebSocketManager = WebSocketManager.getInstance();
|
||||
mWebSocketManager.addCallback(this);
|
||||
|
||||
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
||||
Log.e(TAG, "onSurfaceTextureAvailable: ");
|
||||
// SurfaceTexture准备就绪,开始屏幕捕捉
|
||||
setupVirtualDisplay();
|
||||
}
|
||||
if (mWebSocketManager.isConnected()) {
|
||||
btConnect.setEnabled(false);
|
||||
} else {
|
||||
btConnect.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
|
||||
Log.e(TAG, "onSurfaceTextureSizeChanged: ");
|
||||
}
|
||||
// 使用 Service 中统一的 EglBase 保证 Context 兼容性
|
||||
if (ScreenCaptureService2.rootEglBase == null) {
|
||||
ScreenCaptureService2.rootEglBase = EglBase.create();
|
||||
}
|
||||
localRender.init(ScreenCaptureService2.rootEglBase.getEglBaseContext(), null);
|
||||
localRender.setMirror(false); // 屏幕采集通常不需要镜像
|
||||
localRender.setEnableHardwareScaler(true);
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||
Log.e(TAG, "onSurfaceTextureDestroyed: ");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
|
||||
// Log.e(TAG, "onSurfaceTextureUpdated: ");
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化屏幕参数
|
||||
screenWidth = getResources().getDisplayMetrics().widthPixels;
|
||||
screenHeight = getResources().getDisplayMetrics().heightPixels;
|
||||
screenDpi = getResources().getDisplayMetrics().densityDpi;
|
||||
|
||||
// 初始化MediaProjectionManager
|
||||
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
||||
|
||||
// 检查权限
|
||||
btConnect.setOnClickListener(v -> reconnectWebSocket());
|
||||
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
||||
|
||||
// 初始化WebRTC EGL环境
|
||||
rootEglBase = EglBase.create();
|
||||
localRender.init(rootEglBase.getEglBaseContext(), null);
|
||||
localRender.setMirror(true);
|
||||
localRender.setEnableHardwareScaler(true);
|
||||
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
||||
|
||||
setupServiceListener();
|
||||
|
||||
Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
} else {
|
||||
startService(serviceIntent);
|
||||
}
|
||||
|
||||
registerReceiver(mBroadcastReceiver, new IntentFilter(ScreenCaptureService2.ACTION_PERMISSION_ACTION));
|
||||
}
|
||||
|
||||
private static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
|
||||
private void setupServiceListener() {
|
||||
ScreenCaptureService2.setStatusListener(new ScreenCaptureService2.StatusListener() {
|
||||
@Override
|
||||
public void onStatusChanged(String status) {
|
||||
runOnUiThread(() -> tvStatus.setText(status));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoTrack(VideoTrack track) {
|
||||
runOnUiThread(() -> track.addSink(localRender));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reconnectWebSocket() {
|
||||
mWebSocketManager.connect();
|
||||
}
|
||||
|
||||
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.e(TAG, "onReceive: " + intent.getAction());
|
||||
checkPermissionsAndStart();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查权限并启动采集+推流
|
||||
*/
|
||||
private void checkPermissionsAndStart() {
|
||||
List<String> neededPermissions = new ArrayList<>();
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
|
||||
neededPermissions.add(Manifest.permission.INTERNET);
|
||||
}
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
// ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
|
||||
// neededPermissions.add(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION);
|
||||
// }
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33 &&
|
||||
ContextCompat.checkSelfPermission(this, FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
|
||||
neededPermissions.add(FOREGROUND_SERVICE_MEDIA_PROJECTION);
|
||||
}
|
||||
|
||||
if (!neededPermissions.isEmpty()) {
|
||||
ActivityCompat.requestPermissions(this, neededPermissions.toArray(new String[0]), REQUEST_CODE_PERMISSIONS);
|
||||
String permission = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
|
||||
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, new String[]{permission}, REQUEST_CODE_PERMISSIONS);
|
||||
} else {
|
||||
// 启动前台服务
|
||||
startForegroundService();
|
||||
// 权限已满足,请求屏幕录制授权
|
||||
startScreenCaptureAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起屏幕录制授权请求
|
||||
*/
|
||||
private void startScreenCaptureAuthorization() {
|
||||
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
|
||||
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
|
||||
}
|
||||
|
||||
private void initWebSocket() {
|
||||
Log.e(TAG, "initWebSocket: ");
|
||||
// 初始化 WebSocket 管理器
|
||||
webSocketManager = WebSocketManager.getInstance();
|
||||
// String wsUrl = "ws://175.178.213.60:2310/signaling/981964879";
|
||||
String wsUrl = "ws://192.168.100.111:2310/signaling/981964879";
|
||||
webSocketManager.init(wsUrl, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化WebRTC核心组件
|
||||
*/
|
||||
private void initWebRTC() {
|
||||
// 1. 初始化PeerConnectionFactory
|
||||
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(this)
|
||||
.setEnableInternalTracer(true)
|
||||
.setFieldTrials("WebRTC-H264HighProfile/Enabled/") // 可选:启用H264
|
||||
.createInitializationOptions();
|
||||
PeerConnectionFactory.initialize(initOptions);
|
||||
|
||||
// 2. 创建PeerConnectionFactory实例
|
||||
PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(factoryOptions)
|
||||
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(
|
||||
EglBase.create().getEglBaseContext(), true, true))
|
||||
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(
|
||||
EglBase.create().getEglBaseContext()))
|
||||
.createPeerConnectionFactory();
|
||||
|
||||
// 3. 创建自定义屏幕采集器
|
||||
screenVideoCapturer = new ScreenVideoCapturer();
|
||||
|
||||
// 4. 创建VideoSource和VideoTrack
|
||||
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("ScreenCaptureThread", rootEglBase.getEglBaseContext());
|
||||
videoSource = peerConnectionFactory.createVideoSource(screenVideoCapturer.isScreencast());
|
||||
screenVideoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
|
||||
screenVideoCapturer.startCapture(screenWidth, screenHeight, 30); // 30fps
|
||||
|
||||
// 5. 创建视频轨道并绑定到本地渲染
|
||||
videoTrack = peerConnectionFactory.createVideoTrack("screen_video_track", videoSource);
|
||||
videoTrack.addSink(localRender);
|
||||
|
||||
// 6. 创建PeerConnection(需替换为实际的ICE服务器配置)
|
||||
List<PeerConnection.IceServer> iceServers = getIceServers();
|
||||
|
||||
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
||||
// 配置 ICE 传输策略(可选,根据网络环境调整)
|
||||
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // 通常禁用 TCP 候选,使用 UDP
|
||||
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; // 推荐启用 BUNDLE 以节省资源
|
||||
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; // 持续收集 ICE 候选
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
|
||||
@Override
|
||||
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
|
||||
Log.d(TAG, "PeerConnection onSignalingChange: " + signalingState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
Log.d(TAG, "PeerConnection onIceConnectionChange: " + iceConnectionState);
|
||||
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
|
||||
runOnUiThread(() -> Toast.makeText(WebRTCScreenCaptureActivity.this, "WebRTC连接成功", Toast.LENGTH_SHORT).show());
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
|
||||
serviceIntent.setAction(ScreenCaptureService2.ACTION_START_CAPTURE);
|
||||
serviceIntent.putExtra("resultCode", resultCode);
|
||||
serviceIntent.putExtra("data", data);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
} else {
|
||||
startService(serviceIntent);
|
||||
}
|
||||
btnStartPush.setText("正在推流");
|
||||
btnStartPush.setEnabled(false);
|
||||
Toast.makeText(this, "推流服务已启动", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "屏幕录制授权被拒绝", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionReceivingChange(boolean b) {
|
||||
Log.d(TAG, "PeerConnection onIceConnectionReceivingChange: " + b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
|
||||
Log.d(TAG, "PeerConnection onIceGatheringChange: " + iceGatheringState);
|
||||
// ICE候选收集完成后,可将本地SDP发送给远端
|
||||
if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
|
||||
Log.d(TAG, "ICE候选收集完成,可发送本地SDP给远端");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate iceCandidate) {
|
||||
Log.d(TAG, "PeerConnection onIceCandidate: " + iceCandidate);
|
||||
// 将ICE候选发送给远端(实际需通过信令服务器)
|
||||
sendIceCandidateToRemote(iceCandidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
|
||||
Log.d(TAG, "PeerConnection onIceCandidatesRemoved");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "PeerConnection onAddStream: 收到远端流");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "PeerConnection onRemoveStream");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChannel(org.webrtc.DataChannel dataChannel) {
|
||||
Log.d(TAG, "PeerConnection onDataChannel");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenegotiationNeeded() {
|
||||
Log.d(TAG, "PeerConnection onRenegotiationNeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
|
||||
Log.d(TAG, "PeerConnection onAddTrack");
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 创建MediaStream并添加视频轨道
|
||||
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream("screen_stream");
|
||||
mediaStream.addTrack(videoTrack);
|
||||
peerConnection.addStream(mediaStream);
|
||||
|
||||
// 8. 创建Offer(发起端)
|
||||
createOffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建WebRTC Offer
|
||||
*/
|
||||
private void createOffer() {
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")); // 只推流,不接收视频
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")); // 不接收音频
|
||||
|
||||
peerConnection.createOffer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
// 设置本地SDP
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
// 本地SDP设置成功,通过信令服务器发送给远端
|
||||
Log.d(TAG, "本地Offer创建成功: " + sessionDescription.description);
|
||||
sendSdpToRemote(sessionDescription);
|
||||
// 将本地SDP发送给远端(实际需通过信令服务器)
|
||||
remoteSdp = sessionDescription.description; // 示例:实际需发送到远端
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "创建Offer失败: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置本地SDP失败: " + s);
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "创建Offer失败: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
private List<PeerConnection.IceServer> getIceServers() {
|
||||
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||
|
||||
// 添加用户指定的 STUN 服务器
|
||||
PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478")
|
||||
.setUsername("tt")
|
||||
.setPassword("tongtong")
|
||||
.createIceServer();
|
||||
iceServers.add(stunServer);
|
||||
|
||||
// 强烈建议添加备用的公共 STUN 服务器
|
||||
PeerConnection.IceServer publicStunServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
|
||||
.createIceServer();
|
||||
iceServers.add(publicStunServer);
|
||||
|
||||
// 如果您的部署包含 TURN 服务器(用于可靠的中继转发),也应在此添加
|
||||
// PeerConnection.IceServer turnServer = PeerConnection.IceServer.builder("turn:your_turn_server:3478")
|
||||
// .setUsername("your_username")
|
||||
// .setPassword("your_password")
|
||||
// .createIceServer();
|
||||
// iceServers.add(turnServer);
|
||||
|
||||
return iceServers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送SDP给远端信令服务器
|
||||
*/
|
||||
private void sendSdpToRemote(SessionDescription sdp) {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("msg_type", 1);
|
||||
|
||||
JsonObject sdpJson = new JsonObject();
|
||||
sdpJson.addProperty("type", sdp.type.canonicalForm()); // "offer" 或 "answer"
|
||||
sdpJson.addProperty("sdp", sdp.description);
|
||||
// TODO: 2026/3/11 之后改为唯一标识码
|
||||
sdpJson.addProperty("fromUser", "981964879"); // 标识发送方
|
||||
sdpJson.addProperty("timestamp", System.currentTimeMillis());
|
||||
|
||||
jsonObject.add("content", sdpJson);
|
||||
String json = jsonObject.toString();
|
||||
|
||||
Log.d(TAG, "发送SDP到信令服务器: ");
|
||||
Log.e(TAG, "sendSdpToRemote: SDP内容: " + json);
|
||||
|
||||
webSocketManager.sendMessage(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送ICE Candidate给远端信令服务器
|
||||
*/
|
||||
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
|
||||
Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp);
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("msg_type", 2);
|
||||
|
||||
JsonObject iceJson = new JsonObject();
|
||||
iceJson.addProperty("sdpMid", iceCandidate.sdpMid);
|
||||
iceJson.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
|
||||
iceJson.addProperty("sdp", iceCandidate.sdp);
|
||||
|
||||
// TODO: 2026/3/11 之后改为唯一标识码
|
||||
iceJson.addProperty("fromUser", "981964879");
|
||||
iceJson.addProperty("timestamp", System.currentTimeMillis());
|
||||
|
||||
jsonObject.add("content", iceJson);
|
||||
|
||||
String json = jsonObject.toString();
|
||||
|
||||
Log.d(TAG, "发送ICE Candidate到信令服务器: ");
|
||||
Log.e(TAG, "sendIceCandidateToRemote: ICE内容: " + json);
|
||||
webSocketManager.sendMessage(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理信令服务器响应(接收远端的Answer SDP)
|
||||
*/
|
||||
private void handleSignalingResponse(String response) {
|
||||
JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject();
|
||||
;
|
||||
|
||||
// 检查是否包含远端的Answer SDP
|
||||
if (jsonResponse.has("type") && "answer".equals(jsonResponse.get("type").getAsString())) {
|
||||
String remoteSdp = jsonResponse.get("sdp").getAsString();
|
||||
SessionDescription remoteSessionDescription =
|
||||
new SessionDescription(SessionDescription.Type.ANSWER, remoteSdp);
|
||||
|
||||
// 设置远端SDP
|
||||
peerConnection.setRemoteDescription(new SimpleSdpObserver(), remoteSessionDescription);
|
||||
Log.d(TAG, "已设置远端Answer SDP");
|
||||
}
|
||||
|
||||
// 检查是否包含远端的ICE Candidate
|
||||
if (jsonResponse.has("candidate")) {
|
||||
String sdpMid = jsonResponse.get("sdpMid").getAsString();
|
||||
int sdpMLineIndex = jsonResponse.get("sdpMLineIndex").getAsInt();
|
||||
String candidate = jsonResponse.get("candidate").getAsString();
|
||||
|
||||
IceCandidate remoteIceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate);
|
||||
peerConnection.addIceCandidate(remoteIceCandidate);
|
||||
Log.d(TAG, "已添加远端ICE Candidate");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected() {
|
||||
Log.e(TAG, "onConnected: ");
|
||||
tvStatus.setText("websocket已连接");
|
||||
btConnect.setEnabled(false);
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CODE_PERMISSIONS && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startScreenCaptureAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
ScreenCaptureService2.setStatusListener(null);
|
||||
if (localRender != null) {
|
||||
localRender.release();
|
||||
}
|
||||
mWebSocketManager.removeCallback(this);
|
||||
if (mBroadcastReceiver != null) {
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(String sessionId) {
|
||||
Log.e(TAG, "onConnected: " + sessionId);
|
||||
tvStatus.setText("WebSocket已连接");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(String reason) {
|
||||
Log.e(TAG, "onDisconnected: " + reason);
|
||||
tvStatus.setText("websocket已断开:" + reason);
|
||||
btConnect.setEnabled(true);
|
||||
tvStatus.setText("WebSocket已断开");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -498,278 +189,12 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity implements We
|
||||
|
||||
@Override
|
||||
public void onMessage(ByteString bytes) {
|
||||
Log.e(TAG, "onMessage: ");
|
||||
Log.e(TAG, "onMessage: " + bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "onError: " + error);
|
||||
tvStatus.setText("websocket错误:" + error);
|
||||
btConnect.setEnabled(true);
|
||||
tvStatus.setText("WebSocket错误:" + error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的SdpObserver实现
|
||||
*/
|
||||
private static class SimpleSdpObserver implements SdpObserver {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.d(TAG, "SDP设置成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String error) {
|
||||
Log.e(TAG, "创建SDP失败: " + error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String error) {
|
||||
Log.e(TAG, "设置SDP失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义屏幕视频采集器(核心:对接MediaProjection和WebRTC)
|
||||
*/
|
||||
private class ScreenVideoCapturer implements VideoCapturer {
|
||||
private CapturerObserver capturerObserver;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private Surface surface;
|
||||
|
||||
@Override
|
||||
public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
|
||||
this.surfaceTextureHelper = surfaceTextureHelper;
|
||||
this.capturerObserver = capturerObserver;
|
||||
this.surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
|
||||
|
||||
// 创建虚拟显示器,将屏幕内容输出到WebRTC的Surface
|
||||
createVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startCapture(int width, int height, int framerate) {
|
||||
capturerObserver.onCapturerStarted(true);
|
||||
Log.d(TAG, "Screen capturer started: " + width + "x" + height + " " + framerate + "fps");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopCapture() throws InterruptedException {
|
||||
capturerObserver.onCapturerStopped();
|
||||
releaseVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeCaptureFormat(int width, int height, int framerate) {
|
||||
// 适配分辨率变化
|
||||
releaseVirtualDisplay();
|
||||
screenWidth = width;
|
||||
screenHeight = height;
|
||||
createVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
surfaceTextureHelper.dispose();
|
||||
releaseVirtualDisplay();
|
||||
stopForegroundService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScreencast() {
|
||||
return true; // 标记为屏幕采集(非摄像头)
|
||||
}
|
||||
|
||||
private void createVirtualDisplay() {
|
||||
if (mediaProjection == null || surface == null) {
|
||||
return;
|
||||
}
|
||||
virtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"WebRTC-ScreenCapture",
|
||||
screenWidth,
|
||||
screenHeight,
|
||||
screenDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private void releaseVirtualDisplay() {
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动前台服务(Android 10+ 必须)
|
||||
*/
|
||||
private void startForegroundService() {
|
||||
Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
} else {
|
||||
startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止前台服务
|
||||
*/
|
||||
private void stopForegroundService() {
|
||||
stopService(new Intent(this, ScreenCaptureService2.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放虚拟显示器
|
||||
*/
|
||||
private void releaseVirtualDisplay() {
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理权限请求结果
|
||||
*/
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CODE_PERMISSIONS) {
|
||||
boolean allGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allGranted) {
|
||||
startScreenCaptureAuthorization();
|
||||
} else {
|
||||
Toast.makeText(this, "权限申请失败,无法启动推流", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理屏幕录制授权结果
|
||||
*/
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
|
||||
// 授权成功,获取MediaProjection
|
||||
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
|
||||
mediaProjection.registerCallback(new MediaProjection.Callback() {
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
Toast.makeText(WebRTCScreenCaptureActivity.this, "屏幕采集已停止", Toast.LENGTH_SHORT).show();
|
||||
releaseResources();
|
||||
}
|
||||
}, null);
|
||||
|
||||
setupVirtualDisplay();
|
||||
|
||||
// 初始化WebRTC并开始推流
|
||||
initWebRTC();
|
||||
|
||||
// 更新按钮状态
|
||||
btnStartPush.setText("停止推流");
|
||||
btnStartPush.setOnClickListener(v -> releaseResources());
|
||||
} else {
|
||||
Toast.makeText(this, "屏幕录制授权被拒绝", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupVirtualDisplay() {
|
||||
if (mediaProjection == null) return;
|
||||
// 获取屏幕尺寸和密度
|
||||
Log.e(TAG, "setupVirtualDisplay: ");
|
||||
|
||||
// 从TextureView的SurfaceTexture创建Surface
|
||||
Surface surface = new Surface(textureView.getSurfaceTexture());
|
||||
// 创建虚拟显示器,将屏幕内容投射到Surface
|
||||
mVirtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"ScreenCapture",
|
||||
screenWidth, screenHeight, screenDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放所有资源
|
||||
*/
|
||||
private void releaseResources() {
|
||||
// 停止WebRTC
|
||||
if (peerConnection != null) {
|
||||
peerConnection.close();
|
||||
peerConnection = null;
|
||||
}
|
||||
if (videoTrack != null) {
|
||||
videoTrack.dispose();
|
||||
videoTrack = null;
|
||||
}
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
videoSource = null;
|
||||
}
|
||||
if (peerConnectionFactory != null) {
|
||||
peerConnectionFactory.dispose();
|
||||
peerConnectionFactory = null;
|
||||
}
|
||||
|
||||
// 停止屏幕采集
|
||||
if (screenVideoCapturer != null) {
|
||||
try {
|
||||
screenVideoCapturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
screenVideoCapturer.dispose();
|
||||
screenVideoCapturer = null;
|
||||
}
|
||||
|
||||
// 释放MediaProjection
|
||||
if (mediaProjection != null) {
|
||||
mediaProjection.stop();
|
||||
mediaProjection = null;
|
||||
}
|
||||
|
||||
// 停止前台服务
|
||||
stopForegroundService();
|
||||
|
||||
// 释放渲染
|
||||
if (localRender != null) {
|
||||
localRender.release();
|
||||
}
|
||||
// if (rootEglBase != null) {
|
||||
// rootEglBase.release();
|
||||
// }
|
||||
|
||||
if (webSocketManager != null) {
|
||||
webSocketManager.disconnect();
|
||||
}
|
||||
|
||||
// 重置按钮
|
||||
btnStartPush.setText("开始推流");
|
||||
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
releaseResources();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
package com.ttstd.remoteservice.activity.main;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.ttstd.remoteservice.R;
|
||||
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
|
||||
import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.remoteservice.databinding.ActivityMainBinding;
|
||||
import com.ttstd.remoteservice.service.ControlService;
|
||||
import com.ttstd.remoteservice.service.ScreenCaptureService;
|
||||
|
||||
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
|
||||
@@ -67,10 +61,6 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
} else {
|
||||
startService(screenServiceIntent);
|
||||
}
|
||||
|
||||
// 启动指令接收服务
|
||||
Intent controlServiceIntent = new Intent(this, ControlService.class);
|
||||
startService(controlServiceIntent);
|
||||
} else {
|
||||
Log.e(TAG, "屏幕采集权限申请失败");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.alibaba.android.arouter.launcher.ARouter;
|
||||
import com.tencent.bugly.crashreport.CrashReport;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import com.ttstd.remoteservice.BuildConfig;
|
||||
import com.ttstd.remoteservice.network.WebSocketManager;
|
||||
import com.ttstd.remoteservice.utils.SystemUtils;
|
||||
|
||||
|
||||
@@ -59,6 +60,7 @@ public class BaseApplication extends Application {
|
||||
CrashReport.setDeviceId(this, Build.MODEL);
|
||||
xcrash.XCrash.init(this);
|
||||
|
||||
WebSocketManager.init(this.getApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ttstd.remoteservice.bean;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class SignalingMessage implements Serializable {
|
||||
private String type; // "call", "offer", "answer", "iceCandidate", etc.
|
||||
private String senderId; // 发送者ID
|
||||
private String targetId; // 接收者ID
|
||||
private Object payload; // 具体携带的数据(可以是String, SessionDescription, IceCandidate等)
|
||||
|
||||
public SignalingMessage(String type, String senderId, String targetId, Object payload) {
|
||||
this.type = type;
|
||||
this.senderId = senderId;
|
||||
this.targetId = targetId;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public void setSenderId(String senderId) {
|
||||
this.senderId = senderId;
|
||||
}
|
||||
|
||||
public String getTargetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public void setTargetId(String targetId) {
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(Object payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ttstd.remoteservice.bean;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class WebRTCMessage implements Serializable {
|
||||
private String type; // "offer", "answer", "iceCandidate", "call", "hangup", "reject", "accept"
|
||||
private String target;
|
||||
private Object data;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(Object data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,6 @@ public class WebSocketConfig {
|
||||
public static final long readTimeout = 10; // 读取超时时间(秒)
|
||||
public static final long writeTimeout = 10; // 写入超时时间(秒)
|
||||
|
||||
public static final long pingInterval = 10;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public interface WebSocketCallback {
|
||||
/**
|
||||
* 连接成功
|
||||
*/
|
||||
void onConnected();
|
||||
void onConnected(String sessionId);
|
||||
|
||||
/**
|
||||
* 连接断开
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.ttstd.remoteservice.network;
|
||||
|
||||
import android.content.Context;
|
||||
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 com.ttstd.remoteservice.utils.SystemUtils;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -26,15 +29,21 @@ import okio.ByteString;
|
||||
*/
|
||||
public class WebSocketManager {
|
||||
private static final String TAG = "WebSocketManager";
|
||||
|
||||
// private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/";
|
||||
private static final String WEBSOCKET_URL = "ws://192.168.100.224:2310/signaling/";
|
||||
|
||||
private static volatile WebSocketManager instance;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private OkHttpClient client;
|
||||
private final Request request;
|
||||
private WebSocket webSocket;
|
||||
private WebSocketCallback callback;
|
||||
private String wsUrl;
|
||||
private final Set<WebSocketCallback> mCallbackSet = new CopyOnWriteArraySet<>();
|
||||
|
||||
// 连接状态
|
||||
private boolean isConnected = false;
|
||||
private boolean mConnected = false;
|
||||
private boolean shouldReconnect = true;
|
||||
|
||||
private Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
@@ -49,61 +58,66 @@ public class WebSocketManager {
|
||||
// 连接状态锁
|
||||
private final Object connectionLock = new Object();
|
||||
|
||||
private WebSocketManager() {
|
||||
// 私有构造函数
|
||||
private final String mLocalUserId;
|
||||
private final WebSocketListener listener = new InnerWebSocketListener();
|
||||
|
||||
private WebSocketManager(Context context) {
|
||||
this.mContext = context.getApplicationContext();
|
||||
this.mLocalUserId = SystemUtils.getSerial();
|
||||
|
||||
// 创建OkHttpClient,支持WebSocket
|
||||
client = new OkHttpClient.Builder()
|
||||
.pingInterval(WebSocketConfig.pingInterval, TimeUnit.SECONDS) // 保持连接活跃
|
||||
.connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
|
||||
.readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
|
||||
.build();
|
||||
request = new Request.Builder()
|
||||
.url(WEBSOCKET_URL + mLocalUserId)
|
||||
.build();
|
||||
|
||||
webSocket = client.newWebSocket(request, listener);
|
||||
}
|
||||
|
||||
public static WebSocketManager getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (WebSocketManager.class) {
|
||||
if (instance == null) {
|
||||
instance = new WebSocketManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 WebSocket 连接
|
||||
*/
|
||||
public void init(String url, WebSocketCallback callback) {
|
||||
this.wsUrl = url;
|
||||
this.callback = callback;
|
||||
public static void init(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (WebSocketManager.class) {
|
||||
if (instance == null) {
|
||||
instance = new WebSocketManager(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client = new OkHttpClient.Builder()
|
||||
.connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
|
||||
.readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
|
||||
.pingInterval(10, TimeUnit.SECONDS) // 设置心跳
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
public static WebSocketManager getInstance() {
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("WebSocketManager must be initialized first with init(Context)");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
connect();
|
||||
public void addCallback(WebSocketCallback listener) {
|
||||
this.mCallbackSet.add(listener);
|
||||
if (isConnected()) {
|
||||
listener.onConnected("连接成功");
|
||||
}
|
||||
}
|
||||
|
||||
public void removeCallback(WebSocketCallback listener) {
|
||||
this.mCallbackSet.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 WebSocket 连接
|
||||
*/
|
||||
private void connect() {
|
||||
if (wsUrl == null) {
|
||||
Log.e(TAG, "WebSocket URL 不能为 null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(wsUrl)) {
|
||||
Log.e(TAG, "WebSocketConfig or URL is null");
|
||||
return;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(wsUrl)
|
||||
.build();
|
||||
|
||||
webSocket = client.newWebSocket(request, new InnerWebSocketListener());
|
||||
Log.d(TAG, "Connecting to WebSocket: " + wsUrl);
|
||||
|
||||
if (mConnected && webSocket != null) return;
|
||||
webSocket = client.newWebSocket(request, listener);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Connect failed: " + e.getMessage());
|
||||
notifyError("连接失败: " + e.getMessage());
|
||||
@@ -114,7 +128,7 @@ public class WebSocketManager {
|
||||
* 发送文本消息
|
||||
*/
|
||||
public boolean sendMessage(String message) {
|
||||
if (webSocket != null && isConnected) {
|
||||
if (webSocket != null && mConnected) {
|
||||
boolean sent = webSocket.send(message);
|
||||
if (sent) {
|
||||
Log.d(TAG, "Message sent: " + message);
|
||||
@@ -137,7 +151,7 @@ public class WebSocketManager {
|
||||
* 发送二进制消息
|
||||
*/
|
||||
public boolean sendMessage(ByteString bytes) {
|
||||
if (webSocket != null && isConnected) {
|
||||
if (webSocket != null && mConnected) {
|
||||
return webSocket.send(bytes);
|
||||
} else {
|
||||
Log.e(TAG, "WebSocket not connected, cannot send binary message");
|
||||
@@ -162,7 +176,7 @@ public class WebSocketManager {
|
||||
webSocket = null;
|
||||
}
|
||||
|
||||
isConnected = false;
|
||||
mConnected = false;
|
||||
Log.d(TAG, "WebSocket 连接已关闭");
|
||||
reconnectAttempts = 0;
|
||||
}
|
||||
@@ -171,7 +185,7 @@ public class WebSocketManager {
|
||||
* 获取连接状态
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
return mConnected && webSocket != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,10 +197,10 @@ public class WebSocketManager {
|
||||
heartbeatRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isConnected && webSocket != null) {
|
||||
if (mConnected && webSocket != null) {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("msg_type", 0);
|
||||
jsonObject.addProperty("content", "ping");
|
||||
jsonObject.addProperty("type", "ping");
|
||||
jsonObject.addProperty("target", "ping");
|
||||
// 发送心跳消息(可以是空消息或特定协议)
|
||||
boolean sent = webSocket.send(jsonObject.toString());
|
||||
Log.d(TAG, "发送心跳包");
|
||||
@@ -210,7 +224,7 @@ public class WebSocketManager {
|
||||
private void handleDisconnect(String reason) {
|
||||
Log.d(TAG, "Handle disconnect: " + reason);
|
||||
|
||||
isConnected = false;
|
||||
mConnected = false;
|
||||
stopHeartbeat();
|
||||
|
||||
notifyDisconnected(reason);
|
||||
@@ -270,7 +284,7 @@ public class WebSocketManager {
|
||||
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
|
||||
Log.d(TAG, "WebSocket connected successfully");
|
||||
|
||||
isConnected = true;
|
||||
mConnected = true;
|
||||
reconnectAttempts = 0; // 重置重连计数器
|
||||
|
||||
// 连接成功,发送队列中的积压消息
|
||||
@@ -278,8 +292,8 @@ public class WebSocketManager {
|
||||
|
||||
// 在主线程通知连接成功
|
||||
mainHandler.post(() -> {
|
||||
if (callback != null) {
|
||||
callback.onConnected();
|
||||
for (WebSocketCallback callback : mCallbackSet) {
|
||||
callback.onConnected("连接成功");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -293,7 +307,7 @@ public class WebSocketManager {
|
||||
|
||||
// 在主线程通知消息接收
|
||||
mainHandler.post(() -> {
|
||||
if (callback != null) {
|
||||
for (WebSocketCallback callback : mCallbackSet) {
|
||||
callback.onMessage(text);
|
||||
}
|
||||
});
|
||||
@@ -305,7 +319,7 @@ public class WebSocketManager {
|
||||
|
||||
// 在主线程通知消息接收
|
||||
mainHandler.post(() -> {
|
||||
if (callback != null) {
|
||||
for (WebSocketCallback callback : mCallbackSet) {
|
||||
callback.onMessage(bytes);
|
||||
}
|
||||
});
|
||||
@@ -338,7 +352,7 @@ public class WebSocketManager {
|
||||
synchronized (connectionLock) {
|
||||
while (!messageQueue.isEmpty()) {
|
||||
String message = messageQueue.poll();
|
||||
if (message != null && isConnected && webSocket != null) {
|
||||
if (message != null && mConnected && webSocket != null) {
|
||||
boolean sent = webSocket.send(message);
|
||||
if (sent) {
|
||||
Log.d(TAG, "队列消息发送成功: " + message);
|
||||
@@ -382,8 +396,8 @@ public class WebSocketManager {
|
||||
*/
|
||||
private void notifyError(String error) {
|
||||
mainHandler.post(() -> {
|
||||
if (callback != null) {
|
||||
callback.onError(error);
|
||||
for (WebSocketCallback callback : mCallbackSet) {
|
||||
callback.onError("检查网络");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -393,8 +407,8 @@ public class WebSocketManager {
|
||||
*/
|
||||
private void notifyDisconnected(String reason) {
|
||||
mainHandler.post(() -> {
|
||||
if (callback != null) {
|
||||
callback.onDisconnected(reason);
|
||||
for (WebSocketCallback callback : mCallbackSet) {
|
||||
callback.onDisconnected("连接关闭中: " + reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.ttstd.remoteservice.service;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class ControlService extends Service {
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +1,681 @@
|
||||
package com.ttstd.remoteservice.service;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
public class ScreenCaptureService2 extends Service {
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
|
||||
import com.ttstd.remoteservice.bean.SignalingMessage;
|
||||
import com.ttstd.remoteservice.network.WebSocketCallback;
|
||||
import com.ttstd.remoteservice.network.WebSocketManager;
|
||||
import com.ttstd.remoteservice.utils.SystemUtils;
|
||||
|
||||
import org.webrtc.CapturerObserver;
|
||||
import org.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.DefaultVideoEncoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SdpObserver;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class ScreenCaptureService2 extends Service implements WebSocketCallback {
|
||||
private static final String TAG = "ScreenCaptureService2";
|
||||
|
||||
private static final String CHANNEL_ID = "ScreenCaptureChannel";
|
||||
private static final int NOTIFICATION_ID = 1001;
|
||||
|
||||
public static final String ACTION_START_CAPTURE = "com.ttstd.remoteservice.ACTION_START_CAPTURE";
|
||||
public static final String ACTION_PERMISSION_ACTION = "com.ttstd.remoteservice.ACTION_PERMISSION";
|
||||
|
||||
private final Gson mGson = new Gson();
|
||||
|
||||
// WebRTC核心组件
|
||||
public static EglBase rootEglBase;
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
private VideoSource videoSource;
|
||||
private static VideoTrack videoTrack;
|
||||
private MediaProjectionManager mediaProjectionManager;
|
||||
private MediaProjection mediaProjection;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
private ScreenVideoCapturer screenVideoCapturer;
|
||||
|
||||
private static StatusListener statusListener;
|
||||
|
||||
public interface StatusListener {
|
||||
void onStatusChanged(String status);
|
||||
|
||||
void onVideoTrack(VideoTrack track);
|
||||
}
|
||||
|
||||
public static void setStatusListener(StatusListener listener) {
|
||||
statusListener = listener;
|
||||
if (statusListener != null && videoTrack != null) {
|
||||
statusListener.onVideoTrack(videoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
private int screenWidth;
|
||||
private int screenHeight;
|
||||
private int screenDpi;
|
||||
|
||||
private WebSocketManager webSocketManager;
|
||||
private String mRemoteUserId;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.e(TAG, "onCreate: ");
|
||||
createNotificationChannel();
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
// Android 12+ 必须指定前台服务类型
|
||||
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
||||
} else {
|
||||
// 低版本直接启动
|
||||
startForeground(NOTIFICATION_ID, createNotification());
|
||||
}
|
||||
|
||||
webSocketManager = WebSocketManager.getInstance();
|
||||
webSocketManager.addCallback(this);
|
||||
initScreenParams();
|
||||
}
|
||||
|
||||
private void initScreenParams() {
|
||||
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
windowManager.getDefaultDisplay().getRealMetrics(metrics);
|
||||
screenWidth = metrics.widthPixels;
|
||||
screenHeight = metrics.heightPixels;
|
||||
screenDpi = metrics.densityDpi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.e(TAG, "onStartCommand: ");
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
|
||||
if (ACTION_START_CAPTURE.equals(action)) {
|
||||
int resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED);
|
||||
Intent data = intent.getParcelableExtra("data");
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
startScreenCapture(resultCode, data);
|
||||
}
|
||||
} else {
|
||||
// 兼容旧逻辑,如果没有 Action 则根据数据判断
|
||||
int resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED);
|
||||
Intent data = intent.getParcelableExtra("data");
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
startScreenCapture(resultCode, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void startScreenCapture(int resultCode, Intent data) {
|
||||
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
||||
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
|
||||
|
||||
mediaProjection.registerCallback(new MediaProjection.Callback() {
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
releaseResources();
|
||||
}
|
||||
}, null);
|
||||
|
||||
initWebRTC();
|
||||
}
|
||||
|
||||
private void initWebRTC() {
|
||||
Log.e(TAG, "Initializing WebRTC in Service");
|
||||
|
||||
// 1. 初始化PeerConnectionFactory
|
||||
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(this)
|
||||
.setEnableInternalTracer(true)
|
||||
.setFieldTrials("WebRTC-H264HighProfile/Enabled/")
|
||||
.createInitializationOptions();
|
||||
PeerConnectionFactory.initialize(initOptions);
|
||||
|
||||
if (rootEglBase == null) {
|
||||
rootEglBase = EglBase.create();
|
||||
}
|
||||
|
||||
// 2. 创建PeerConnectionFactory实例
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(
|
||||
rootEglBase.getEglBaseContext(), true, true))
|
||||
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(
|
||||
rootEglBase.getEglBaseContext()))
|
||||
.createPeerConnectionFactory();
|
||||
|
||||
// 3. 创建自定义屏幕采集器
|
||||
screenVideoCapturer = new ScreenVideoCapturer();
|
||||
|
||||
// 4. 创建VideoSource和VideoTrack
|
||||
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("ScreenCaptureThread", rootEglBase.getEglBaseContext());
|
||||
videoSource = peerConnectionFactory.createVideoSource(screenVideoCapturer.isScreencast());
|
||||
screenVideoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
|
||||
screenVideoCapturer.startCapture(screenWidth, screenHeight, 30);
|
||||
|
||||
videoTrack = peerConnectionFactory.createVideoTrack("screen_video_track", videoSource);
|
||||
|
||||
if (statusListener != null) {
|
||||
statusListener.onVideoTrack(videoTrack);
|
||||
}
|
||||
|
||||
// 5. 创建PeerConnection
|
||||
List<PeerConnection.IceServer> iceServers = getIceServers();
|
||||
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
||||
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
|
||||
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
|
||||
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
|
||||
@Override
|
||||
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
|
||||
Log.e(TAG, "initWebRTC onSignalingChange: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
Log.e(TAG, "initWebRTC onIceConnectionChange: " + iceConnectionState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionReceivingChange(boolean b) {
|
||||
Log.e(TAG, "initWebRTC onIceConnectionReceivingChange: " + b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
|
||||
Log.e(TAG, "initWebRTC onIceGatheringChange: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate iceCandidate) {
|
||||
Log.e(TAG, "initWebRTC onIceCandidate: ");
|
||||
sendIceCandidateToRemote(iceCandidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
|
||||
Log.e(TAG, "initWebRTC onIceCandidatesRemoved: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream mediaStream) {
|
||||
Log.e(TAG, "initWebRTC onAddStream: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream mediaStream) {
|
||||
Log.e(TAG, "initWebRTC onRemoveStream: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChannel(org.webrtc.DataChannel dataChannel) {
|
||||
Log.e(TAG, "initWebRTC onDataChannel: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenegotiationNeeded() {
|
||||
Log.e(TAG, "initWebRTC onRenegotiationNeeded: ");
|
||||
handleRenegotiation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
|
||||
Log.e(TAG, "initWebRTC onAddTrack: ");
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 添加轨道
|
||||
if (peerConnection != null) {
|
||||
peerConnection.addTrack(videoTrack);
|
||||
}
|
||||
|
||||
// // 7. 创建Offer
|
||||
// if (peerConnection != null) {
|
||||
// createOffer();
|
||||
// }
|
||||
}
|
||||
|
||||
private void handleRenegotiation() {
|
||||
if (peerConnection == null) {
|
||||
Log.e(TAG, "handleRenegotiation: peerConnection is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "开始重新协商,创建新的 Offer");
|
||||
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
|
||||
|
||||
peerConnection.createOffer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.e(TAG, "重新协商 Offer 创建成功");
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.e(TAG, "重新协商本地描述设置成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "重新协商本地描述设置完成,发送 Offer 到远端");
|
||||
sendSdpToRemote(sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "重新协商设置本地描述失败: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "重新协商设置本地描述失败: " + s);
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "重新协商 Offer onSetSuccess");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "重新协商创建 Offer 失败: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "重新协商 Offer onSetFailure: " + s);
|
||||
}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
private List<PeerConnection.IceServer> getIceServers() {
|
||||
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||
// 使用用户指定的 STUN 服务器
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
|
||||
// 备用
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||
return iceServers;
|
||||
}
|
||||
|
||||
private void createOffer() {
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
|
||||
|
||||
peerConnection.createOffer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.e(TAG, "createOffer onCreateSuccess onCreateSuccess: " + sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "createOffer onCreateSuccess onSetSuccess: ");
|
||||
sendSdpToRemote(sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "createOffer onCreateSuccess onCreateFailure: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "createOffer onCreateSuccess onSetFailure: " + s);
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "createOffer onSetSuccess: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "createOffer onCreateFailure: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "createOffer onSetFailure: " + s);
|
||||
}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
private static final String MSG_TYPE_ASK = "ask";
|
||||
private static final String MSG_TYPE_OFFER = "offer";
|
||||
private static final String MSG_TYPE_ANSWER = "answer";
|
||||
private static final String MSG_TYPE_SDP = "sdp";
|
||||
private static final String MSG_TYPE_ICE = "iceCandidate";
|
||||
|
||||
private void sendSdpToRemote(SessionDescription description) {
|
||||
String type = description.type.canonicalForm();
|
||||
SignalingMessage message = new SignalingMessage(type, SystemUtils.getSerial(), mRemoteUserId, description);
|
||||
sendSignalingMessage(type, message);
|
||||
}
|
||||
|
||||
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
|
||||
SignalingMessage message = new SignalingMessage("iceCandidate", SystemUtils.getSerial(), mRemoteUserId, iceCandidate);
|
||||
sendSignalingMessage(MSG_TYPE_ICE, message);
|
||||
}
|
||||
|
||||
private void sendSignalingMessage(String type, SignalingMessage message) {
|
||||
if (webSocketManager == null || !webSocketManager.isConnected()) return;
|
||||
|
||||
// 按照服务器要求的包装格式发送
|
||||
JsonObject wrapper = new JsonObject();
|
||||
wrapper.addProperty("type", type);
|
||||
wrapper.addProperty("target", mRemoteUserId);
|
||||
wrapper.add("data", mGson.toJsonTree(message));
|
||||
|
||||
webSocketManager.sendMessage(wrapper.toString());
|
||||
}
|
||||
|
||||
private void handleSignalingMessage(String message) {
|
||||
try {
|
||||
JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject();
|
||||
if (!jsonObject.has("type")) return;
|
||||
|
||||
String type = jsonObject.get("type").getAsString();
|
||||
if (!jsonObject.has("data") || !jsonObject.get("data").isJsonObject()) {
|
||||
Log.e(TAG, "handleSignalingMessage: 'data' is missing or not an object");
|
||||
return;
|
||||
}
|
||||
JsonObject content = jsonObject.getAsJsonObject("data");
|
||||
|
||||
switch (type) {
|
||||
case MSG_TYPE_ASK:
|
||||
handleAskMessage(content);
|
||||
break;
|
||||
case MSG_TYPE_OFFER:
|
||||
handleOfferMessage(content);
|
||||
break;
|
||||
case MSG_TYPE_ANSWER:
|
||||
handleAnswerMessage(content);
|
||||
break;
|
||||
case MSG_TYPE_SDP:
|
||||
handleSdpMessage(content);
|
||||
break;
|
||||
case MSG_TYPE_ICE:
|
||||
handleIceCandidateMessage(content);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析信令消息失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
int mAnalogConnection = 1;
|
||||
|
||||
/**
|
||||
* 收到连接请求
|
||||
*
|
||||
* @param content
|
||||
*/
|
||||
private void handleAskMessage(JsonObject content) {
|
||||
SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
|
||||
if (signalingMessage != null) {
|
||||
String senderId = signalingMessage.getSenderId();
|
||||
mRemoteUserId = senderId;
|
||||
Log.e(TAG, "收到来自 " + senderId + " 的 连接请求");
|
||||
Toast.makeText(this, "收到来自 " + senderId + " 的 连接请求", Toast.LENGTH_SHORT).show();
|
||||
// TODO: 2026/6/4 弹窗询问是否接受
|
||||
if (mAnalogConnection % 2 == 0) {
|
||||
sendRejectMessage(senderId);
|
||||
} else {
|
||||
sendAcceptMessage(senderId);
|
||||
}
|
||||
}
|
||||
|
||||
mAnalogConnection++;
|
||||
}
|
||||
|
||||
private void sendRejectMessage(String senderId) {
|
||||
Log.e(TAG, "sendRejectMessage: reject " + senderId);
|
||||
SignalingMessage message = new SignalingMessage("reject", SystemUtils.getSerial(), senderId, null);
|
||||
sendSignalingMessage("reject", message);
|
||||
}
|
||||
|
||||
private void sendAcceptMessage(String senderId) {
|
||||
Log.e(TAG, "sendAcceptMessage: accept " + senderId);
|
||||
SignalingMessage message = new SignalingMessage("accept", SystemUtils.getSerial(), senderId, null);
|
||||
sendSignalingMessage("accept", message);
|
||||
Intent intent = new Intent(ACTION_PERMISSION_ACTION);
|
||||
// intent.setAction(ACTION_PERMISSION_ACTION);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
|
||||
private void handleOfferMessage(JsonObject content) {
|
||||
SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
|
||||
if (signalingMessage != null) {
|
||||
mRemoteUserId = signalingMessage.getSenderId();
|
||||
Log.e(TAG, "收到来自 " + mRemoteUserId + " 的 Offer");
|
||||
}
|
||||
// TODO: 2026/6/4 暂时不需要接受远程逻辑
|
||||
// 复用 handleSdpMessage 的逻辑来设置远程描述并创建 Answer
|
||||
handleSdpMessage(content);
|
||||
}
|
||||
|
||||
private void handleAnswerMessage(JsonObject content) {
|
||||
SignalingMessage signalingMessage = mGson.fromJson(content, SignalingMessage.class);
|
||||
if (signalingMessage != null) {
|
||||
mRemoteUserId = signalingMessage.getSenderId();
|
||||
Log.e(TAG, "收到来自 " + mRemoteUserId + " 的 Answer");
|
||||
}
|
||||
handleSdpMessage(content);
|
||||
}
|
||||
|
||||
|
||||
private void handleSdpMessage(JsonObject content) {
|
||||
if (peerConnection == null) return;
|
||||
|
||||
if (!content.has("type") || !content.has("payload")) {
|
||||
Log.e(TAG, "handleSdpMessage: 'type' or 'payload' missing");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = content.get("type").getAsString();
|
||||
if (!content.get("payload").isJsonObject()) {
|
||||
Log.e(TAG, "handleSdpMessage: 'payload' is not an object, type=" + type);
|
||||
return;
|
||||
}
|
||||
JsonObject payload = content.getAsJsonObject("payload");
|
||||
|
||||
if (!payload.has("description")) {
|
||||
Log.e(TAG, "handleSdpMessage: 'payload' missing 'description'");
|
||||
return;
|
||||
}
|
||||
String sdpDescription = payload.get("description").getAsString();
|
||||
|
||||
if ("offer".equals(type)) {
|
||||
peerConnection.setRemoteDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "设置远端 Offer 成功,正在创建 Answer");
|
||||
createAnswer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置远端 Offer 失败: " + s);
|
||||
}
|
||||
}, new SessionDescription(SessionDescription.Type.OFFER, sdpDescription));
|
||||
} else if ("answer".equals(type)) {
|
||||
peerConnection.setRemoteDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.e(TAG, "设置远端 Answer 成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置远端 Answer 失败: " + s);
|
||||
}
|
||||
}, new SessionDescription(SessionDescription.Type.ANSWER, sdpDescription));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIceCandidateMessage(JsonObject content) {
|
||||
if (peerConnection == null) return;
|
||||
|
||||
if (!content.has("payload") || !content.get("payload").isJsonObject()) {
|
||||
Log.e(TAG, "handleIceCandidateMessage: 'payload' is missing or not an object");
|
||||
return;
|
||||
}
|
||||
JsonObject payload = content.getAsJsonObject("payload");
|
||||
if (!payload.has("sdpMid") || !payload.has("sdpMLineIndex") || !payload.has("sdp")) {
|
||||
Log.e(TAG, "handleIceCandidateMessage: payload missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
String sdpMid = payload.get("sdpMid").getAsString();
|
||||
int sdpMLineIndex = payload.get("sdpMLineIndex").getAsInt();
|
||||
String candidate = payload.get("sdp").getAsString();
|
||||
peerConnection.addIceCandidate(new IceCandidate(sdpMid, sdpMLineIndex, candidate));
|
||||
}
|
||||
|
||||
private void createAnswer() {
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
// 作为被控端,我们通常只发视频,不收视频/音频(或者根据需要配置)
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
|
||||
|
||||
peerConnection.createAnswer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
sendSdpToRemote(sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onConnected(String sessionId) {
|
||||
Log.e(TAG, "WebSocket connected");
|
||||
if (statusListener != null) {
|
||||
statusListener.onStatusChanged("WebSocket已连接");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(String reason) {
|
||||
Log.e(TAG, "WebSocket disconnected: " + reason);
|
||||
if (statusListener != null) {
|
||||
statusListener.onStatusChanged("WebSocket已断开: " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
Log.e(TAG, "onMessage: " + message);
|
||||
handleSignalingMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ByteString bytes) {
|
||||
Log.e(TAG, "onMessage ByteString: " + bytes.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "WebSocket error: " + error);
|
||||
if (statusListener != null) {
|
||||
statusListener.onStatusChanged("WebSocket错误: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -39,11 +684,6 @@ public class ScreenCaptureService2 extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_STICKY; // 服务被杀死后尝试重启
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
@@ -64,4 +704,117 @@ public class ScreenCaptureService2 extends Service {
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseResources() {
|
||||
if (peerConnection != null) {
|
||||
peerConnection.close();
|
||||
peerConnection = null;
|
||||
}
|
||||
if (videoTrack != null) {
|
||||
videoTrack.dispose();
|
||||
videoTrack = null;
|
||||
}
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
videoSource = null;
|
||||
}
|
||||
if (peerConnectionFactory != null) {
|
||||
peerConnectionFactory.dispose();
|
||||
peerConnectionFactory = null;
|
||||
}
|
||||
if (screenVideoCapturer != null) {
|
||||
try {
|
||||
screenVideoCapturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
screenVideoCapturer.dispose();
|
||||
screenVideoCapturer = null;
|
||||
}
|
||||
if (mediaProjection != null) {
|
||||
mediaProjection.stop();
|
||||
mediaProjection = null;
|
||||
}
|
||||
if (rootEglBase != null) {
|
||||
rootEglBase.release();
|
||||
rootEglBase = null;
|
||||
}
|
||||
webSocketManager.removeCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
releaseResources();
|
||||
}
|
||||
|
||||
private class ScreenVideoCapturer implements VideoCapturer {
|
||||
private CapturerObserver capturerObserver;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private Surface surface;
|
||||
|
||||
@Override
|
||||
public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
|
||||
this.surfaceTextureHelper = surfaceTextureHelper;
|
||||
this.capturerObserver = capturerObserver;
|
||||
|
||||
// 设置 SurfaceTexture 的尺寸,防止默认尺寸导致画面拉伸或无法采集
|
||||
surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(screenWidth, screenHeight);
|
||||
this.surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
|
||||
|
||||
// 监听新帧并交给 capturerObserver 处理,这是显示画面的关键
|
||||
surfaceTextureHelper.startListening(videoFrame -> {
|
||||
capturerObserver.onFrameCaptured(videoFrame);
|
||||
});
|
||||
|
||||
createVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startCapture(int width, int height, int framerate) {
|
||||
capturerObserver.onCapturerStarted(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopCapture() throws InterruptedException {
|
||||
capturerObserver.onCapturerStopped();
|
||||
releaseVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeCaptureFormat(int width, int height, int framerate) {
|
||||
releaseVirtualDisplay();
|
||||
createVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (surfaceTextureHelper != null) {
|
||||
surfaceTextureHelper.dispose();
|
||||
}
|
||||
releaseVirtualDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScreencast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createVirtualDisplay() {
|
||||
if (mediaProjection == null || surface == null) return;
|
||||
virtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"WebRTC-ScreenCapture",
|
||||
screenWidth, screenHeight, screenDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
);
|
||||
}
|
||||
|
||||
private void releaseVirtualDisplay() {
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.ttstd.remoteservice.BuildConfig;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
@@ -18,6 +20,9 @@ public class SystemUtils {
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
public static String getSerial() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return "981964879";
|
||||
}
|
||||
String serial = "unknow";
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
|
||||
|
||||
Reference in New Issue
Block a user