feat: 交换信令
This commit is contained in:
@@ -43,8 +43,6 @@
|
|||||||
android:name=".service.ScreenCaptureService"
|
android:name=".service.ScreenCaptureService"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
|
||||||
<!-- 指令执行服务 -->
|
|
||||||
<service android:name=".service.ControlService" />
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.ScreenCaptureService2"
|
android:name=".service.ScreenCaptureService2"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
package com.ttstd.remoteservice.activity;
|
package com.ttstd.remoteservice.activity;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.PackageManager;
|
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.media.projection.MediaProjectionManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.TextureView;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@@ -25,470 +19,167 @@ 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.WebSocketCallback;
|
||||||
import com.ttstd.remoteservice.network.WebSocketManager;
|
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.DefaultVideoDecoderFactory;
|
|
||||||
import org.webrtc.DefaultVideoEncoderFactory;
|
|
||||||
import org.webrtc.EglBase;
|
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.SurfaceViewRenderer;
|
||||||
import org.webrtc.VideoCapturer;
|
|
||||||
import org.webrtc.VideoSource;
|
|
||||||
import org.webrtc.VideoTrack;
|
import org.webrtc.VideoTrack;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
|
||||||
public class WebRTCScreenCaptureActivity extends AppCompatActivity implements WebSocketCallback {
|
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;
|
||||||
private static final int REQUEST_CODE_PERMISSIONS = 1002;
|
private static final int REQUEST_CODE_PERMISSIONS = 1002;
|
||||||
|
|
||||||
// WebRTC核心组件
|
private WebSocketManager mWebSocketManager;
|
||||||
private PeerConnectionFactory peerConnectionFactory;
|
|
||||||
private PeerConnection peerConnection;
|
|
||||||
private VideoSource videoSource;
|
|
||||||
private VideoTrack videoTrack;
|
|
||||||
private EglBase rootEglBase;
|
|
||||||
|
|
||||||
// 屏幕采集相关
|
|
||||||
private MediaProjectionManager mediaProjectionManager;
|
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 TextView tvStatus;
|
||||||
private Button btConnect, btnStartPush;
|
private Button btConnect, btnStartPush;
|
||||||
private SurfaceViewRenderer localRender; // 本地预览(可选)
|
private SurfaceViewRenderer localRender;
|
||||||
|
|
||||||
private TextureView textureView;
|
|
||||||
|
|
||||||
private VirtualDisplay mVirtualDisplay;
|
|
||||||
|
|
||||||
// 远端信令信息(示例:实际需从信令服务器获取)
|
|
||||||
private String remoteSdp = ""; // 替换为远端实际SDP
|
|
||||||
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);
|
tvStatus = findViewById(R.id.tv_status);
|
||||||
btConnect = findViewById(R.id.bt_connect);
|
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);
|
|
||||||
|
|
||||||
btConnect.setOnClickListener(new View.OnClickListener() {
|
mWebSocketManager = WebSocketManager.getInstance();
|
||||||
@Override
|
mWebSocketManager.addCallback(this);
|
||||||
public void onClick(View v) {
|
|
||||||
initWebSocket();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
if (mWebSocketManager.isConnected()) {
|
||||||
@Override
|
btConnect.setEnabled(false);
|
||||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
} else {
|
||||||
Log.e(TAG, "onSurfaceTextureAvailable: ");
|
btConnect.setEnabled(true);
|
||||||
// SurfaceTexture准备就绪,开始屏幕捕捉
|
}
|
||||||
setupVirtualDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// 使用 Service 中统一的 EglBase 保证 Context 兼容性
|
||||||
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
|
if (ScreenCaptureService2.rootEglBase == null) {
|
||||||
Log.e(TAG, "onSurfaceTextureSizeChanged: ");
|
ScreenCaptureService2.rootEglBase = EglBase.create();
|
||||||
}
|
}
|
||||||
|
localRender.init(ScreenCaptureService2.rootEglBase.getEglBaseContext(), null);
|
||||||
|
localRender.setMirror(false); // 屏幕采集通常不需要镜像
|
||||||
|
localRender.setEnableHardwareScaler(true);
|
||||||
|
|
||||||
@Override
|
btConnect.setOnClickListener(v -> reconnectWebSocket());
|
||||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
|
||||||
Log.e(TAG, "onSurfaceTextureDestroyed: ");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
|
|
||||||
// Log.e(TAG, "onSurfaceTextureUpdated: ");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化屏幕参数
|
|
||||||
screenWidth = getResources().getDisplayMetrics().widthPixels;
|
|
||||||
screenHeight = getResources().getDisplayMetrics().heightPixels;
|
|
||||||
screenDpi = getResources().getDisplayMetrics().densityDpi;
|
|
||||||
|
|
||||||
// 初始化MediaProjectionManager
|
|
||||||
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
|
||||||
|
|
||||||
// 检查权限
|
|
||||||
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
|
||||||
|
|
||||||
// 初始化WebRTC EGL环境
|
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
||||||
rootEglBase = EglBase.create();
|
|
||||||
localRender.init(rootEglBase.getEglBaseContext(), null);
|
setupServiceListener();
|
||||||
localRender.setMirror(true);
|
|
||||||
localRender.setEnableHardwareScaler(true);
|
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() {
|
private void checkPermissionsAndStart() {
|
||||||
List<String> neededPermissions = new ArrayList<>();
|
String permission = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
neededPermissions.add(Manifest.permission.INTERNET);
|
ActivityCompat.requestPermissions(this, new String[]{permission}, REQUEST_CODE_PERMISSIONS);
|
||||||
}
|
|
||||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
||||||
// ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
// neededPermissions.add(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 33 &&
|
|
||||||
ContextCompat.checkSelfPermission(this, FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
neededPermissions.add(FOREGROUND_SERVICE_MEDIA_PROJECTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!neededPermissions.isEmpty()) {
|
|
||||||
ActivityCompat.requestPermissions(this, neededPermissions.toArray(new String[0]), REQUEST_CODE_PERMISSIONS);
|
|
||||||
} else {
|
} else {
|
||||||
// 启动前台服务
|
|
||||||
startForegroundService();
|
|
||||||
// 权限已满足,请求屏幕录制授权
|
|
||||||
startScreenCaptureAuthorization();
|
startScreenCaptureAuthorization();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发起屏幕录制授权请求
|
|
||||||
*/
|
|
||||||
private void startScreenCaptureAuthorization() {
|
private void startScreenCaptureAuthorization() {
|
||||||
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
|
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
|
||||||
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
|
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initWebSocket() {
|
@Override
|
||||||
Log.e(TAG, "initWebSocket: ");
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
// 初始化 WebSocket 管理器
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
webSocketManager = WebSocketManager.getInstance();
|
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
|
||||||
// String wsUrl = "ws://175.178.213.60:2310/signaling/981964879";
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
String wsUrl = "ws://192.168.100.111:2310/signaling/981964879";
|
Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
|
||||||
webSocketManager.init(wsUrl, this);
|
serviceIntent.setAction(ScreenCaptureService2.ACTION_START_CAPTURE);
|
||||||
}
|
serviceIntent.putExtra("resultCode", resultCode);
|
||||||
|
serviceIntent.putExtra("data", data);
|
||||||
/**
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
* 初始化WebRTC核心组件
|
startForegroundService(serviceIntent);
|
||||||
*/
|
} else {
|
||||||
private void initWebRTC() {
|
startService(serviceIntent);
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
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
|
@Override
|
||||||
public void onConnected() {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
Log.e(TAG, "onConnected: ");
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
tvStatus.setText("websocket已连接");
|
if (requestCode == REQUEST_CODE_PERMISSIONS && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
btConnect.setEnabled(false);
|
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
|
@Override
|
||||||
public void onDisconnected(String reason) {
|
public void onDisconnected(String reason) {
|
||||||
Log.e(TAG, "onDisconnected: " + reason);
|
Log.e(TAG, "onDisconnected: " + reason);
|
||||||
tvStatus.setText("websocket已断开:" + reason);
|
tvStatus.setText("WebSocket已断开");
|
||||||
btConnect.setEnabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -498,278 +189,12 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity implements We
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(ByteString bytes) {
|
public void onMessage(ByteString bytes) {
|
||||||
Log.e(TAG, "onMessage: ");
|
Log.e(TAG, "onMessage: " + bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(String error) {
|
public void onError(String error) {
|
||||||
Log.e(TAG, "onError: " + error);
|
Log.e(TAG, "onError: " + error);
|
||||||
tvStatus.setText("websocket错误:" + 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义屏幕视频采集器(核心:对接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;
|
package com.ttstd.remoteservice.activity.main;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.media.projection.MediaProjectionManager;
|
import android.media.projection.MediaProjectionManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.ttstd.remoteservice.R;
|
import com.ttstd.remoteservice.R;
|
||||||
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
|
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
|
||||||
import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity;
|
import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity;
|
||||||
import com.ttstd.remoteservice.databinding.ActivityMainBinding;
|
import com.ttstd.remoteservice.databinding.ActivityMainBinding;
|
||||||
import com.ttstd.remoteservice.service.ControlService;
|
|
||||||
import com.ttstd.remoteservice.service.ScreenCaptureService;
|
import com.ttstd.remoteservice.service.ScreenCaptureService;
|
||||||
|
|
||||||
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
|
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
|
||||||
@@ -67,10 +61,6 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
|||||||
} else {
|
} else {
|
||||||
startService(screenServiceIntent);
|
startService(screenServiceIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动指令接收服务
|
|
||||||
Intent controlServiceIntent = new Intent(this, ControlService.class);
|
|
||||||
startService(controlServiceIntent);
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "屏幕采集权限申请失败");
|
Log.e(TAG, "屏幕采集权限申请失败");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.alibaba.android.arouter.launcher.ARouter;
|
|||||||
import com.tencent.bugly.crashreport.CrashReport;
|
import com.tencent.bugly.crashreport.CrashReport;
|
||||||
import com.tencent.mmkv.MMKV;
|
import com.tencent.mmkv.MMKV;
|
||||||
import com.ttstd.remoteservice.BuildConfig;
|
import com.ttstd.remoteservice.BuildConfig;
|
||||||
|
import com.ttstd.remoteservice.network.WebSocketManager;
|
||||||
import com.ttstd.remoteservice.utils.SystemUtils;
|
import com.ttstd.remoteservice.utils.SystemUtils;
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ public class BaseApplication extends Application {
|
|||||||
CrashReport.setDeviceId(this, Build.MODEL);
|
CrashReport.setDeviceId(this, Build.MODEL);
|
||||||
xcrash.XCrash.init(this);
|
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 readTimeout = 10; // 读取超时时间(秒)
|
||||||
public static final long writeTimeout = 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;
|
package com.ttstd.remoteservice.network;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.ttstd.remoteservice.config.WebSocketConfig;
|
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.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
@@ -26,15 +29,21 @@ import okio.ByteString;
|
|||||||
*/
|
*/
|
||||||
public class WebSocketManager {
|
public class WebSocketManager {
|
||||||
private static final String TAG = "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 static volatile WebSocketManager instance;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
private OkHttpClient client;
|
private OkHttpClient client;
|
||||||
|
private final Request request;
|
||||||
private WebSocket webSocket;
|
private WebSocket webSocket;
|
||||||
private WebSocketCallback callback;
|
private final Set<WebSocketCallback> mCallbackSet = new CopyOnWriteArraySet<>();
|
||||||
private String wsUrl;
|
|
||||||
|
|
||||||
// 连接状态
|
// 连接状态
|
||||||
private boolean isConnected = false;
|
private boolean mConnected = false;
|
||||||
private boolean shouldReconnect = true;
|
private boolean shouldReconnect = true;
|
||||||
|
|
||||||
private Handler mainHandler = new Handler(Looper.getMainLooper());
|
private Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
@@ -49,61 +58,66 @@ public class WebSocketManager {
|
|||||||
// 连接状态锁
|
// 连接状态锁
|
||||||
private final Object connectionLock = new Object();
|
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 连接
|
* 初始化 WebSocket 连接
|
||||||
*/
|
*/
|
||||||
public void init(String url, WebSocketCallback callback) {
|
public static void init(Context context) {
|
||||||
this.wsUrl = url;
|
if (instance == null) {
|
||||||
this.callback = callback;
|
synchronized (WebSocketManager.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new WebSocketManager(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client = new OkHttpClient.Builder()
|
public static WebSocketManager getInstance() {
|
||||||
.connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
|
if (instance == null) {
|
||||||
.readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
|
throw new IllegalStateException("WebSocketManager must be initialized first with init(Context)");
|
||||||
.writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
|
}
|
||||||
.pingInterval(10, TimeUnit.SECONDS) // 设置心跳
|
return instance;
|
||||||
.retryOnConnectionFailure(true)
|
}
|
||||||
.build();
|
|
||||||
|
|
||||||
connect();
|
public void addCallback(WebSocketCallback listener) {
|
||||||
|
this.mCallbackSet.add(listener);
|
||||||
|
if (isConnected()) {
|
||||||
|
listener.onConnected("连接成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCallback(WebSocketCallback listener) {
|
||||||
|
this.mCallbackSet.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建立 WebSocket 连接
|
* 建立 WebSocket 连接
|
||||||
*/
|
*/
|
||||||
private void connect() {
|
public 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 {
|
try {
|
||||||
Request request = new Request.Builder()
|
if (mConnected && webSocket != null) return;
|
||||||
.url(wsUrl)
|
webSocket = client.newWebSocket(request, listener);
|
||||||
.build();
|
|
||||||
|
|
||||||
webSocket = client.newWebSocket(request, new InnerWebSocketListener());
|
|
||||||
Log.d(TAG, "Connecting to WebSocket: " + wsUrl);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Connect failed: " + e.getMessage());
|
Log.e(TAG, "Connect failed: " + e.getMessage());
|
||||||
notifyError("连接失败: " + e.getMessage());
|
notifyError("连接失败: " + e.getMessage());
|
||||||
@@ -114,7 +128,7 @@ public class WebSocketManager {
|
|||||||
* 发送文本消息
|
* 发送文本消息
|
||||||
*/
|
*/
|
||||||
public boolean sendMessage(String message) {
|
public boolean sendMessage(String message) {
|
||||||
if (webSocket != null && isConnected) {
|
if (webSocket != null && mConnected) {
|
||||||
boolean sent = webSocket.send(message);
|
boolean sent = webSocket.send(message);
|
||||||
if (sent) {
|
if (sent) {
|
||||||
Log.d(TAG, "Message sent: " + message);
|
Log.d(TAG, "Message sent: " + message);
|
||||||
@@ -137,7 +151,7 @@ public class WebSocketManager {
|
|||||||
* 发送二进制消息
|
* 发送二进制消息
|
||||||
*/
|
*/
|
||||||
public boolean sendMessage(ByteString bytes) {
|
public boolean sendMessage(ByteString bytes) {
|
||||||
if (webSocket != null && isConnected) {
|
if (webSocket != null && mConnected) {
|
||||||
return webSocket.send(bytes);
|
return webSocket.send(bytes);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "WebSocket not connected, cannot send binary message");
|
Log.e(TAG, "WebSocket not connected, cannot send binary message");
|
||||||
@@ -162,7 +176,7 @@ public class WebSocketManager {
|
|||||||
webSocket = null;
|
webSocket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected = false;
|
mConnected = false;
|
||||||
Log.d(TAG, "WebSocket 连接已关闭");
|
Log.d(TAG, "WebSocket 连接已关闭");
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
}
|
}
|
||||||
@@ -171,7 +185,7 @@ public class WebSocketManager {
|
|||||||
* 获取连接状态
|
* 获取连接状态
|
||||||
*/
|
*/
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
return isConnected;
|
return mConnected && webSocket != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,10 +197,10 @@ public class WebSocketManager {
|
|||||||
heartbeatRunnable = new Runnable() {
|
heartbeatRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (isConnected && webSocket != null) {
|
if (mConnected && webSocket != null) {
|
||||||
JsonObject jsonObject = new JsonObject();
|
JsonObject jsonObject = new JsonObject();
|
||||||
jsonObject.addProperty("msg_type", 0);
|
jsonObject.addProperty("type", "ping");
|
||||||
jsonObject.addProperty("content", "ping");
|
jsonObject.addProperty("target", "ping");
|
||||||
// 发送心跳消息(可以是空消息或特定协议)
|
// 发送心跳消息(可以是空消息或特定协议)
|
||||||
boolean sent = webSocket.send(jsonObject.toString());
|
boolean sent = webSocket.send(jsonObject.toString());
|
||||||
Log.d(TAG, "发送心跳包");
|
Log.d(TAG, "发送心跳包");
|
||||||
@@ -210,7 +224,7 @@ public class WebSocketManager {
|
|||||||
private void handleDisconnect(String reason) {
|
private void handleDisconnect(String reason) {
|
||||||
Log.d(TAG, "Handle disconnect: " + reason);
|
Log.d(TAG, "Handle disconnect: " + reason);
|
||||||
|
|
||||||
isConnected = false;
|
mConnected = false;
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
|
|
||||||
notifyDisconnected(reason);
|
notifyDisconnected(reason);
|
||||||
@@ -270,7 +284,7 @@ public class WebSocketManager {
|
|||||||
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
|
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
|
||||||
Log.d(TAG, "WebSocket connected successfully");
|
Log.d(TAG, "WebSocket connected successfully");
|
||||||
|
|
||||||
isConnected = true;
|
mConnected = true;
|
||||||
reconnectAttempts = 0; // 重置重连计数器
|
reconnectAttempts = 0; // 重置重连计数器
|
||||||
|
|
||||||
// 连接成功,发送队列中的积压消息
|
// 连接成功,发送队列中的积压消息
|
||||||
@@ -278,8 +292,8 @@ public class WebSocketManager {
|
|||||||
|
|
||||||
// 在主线程通知连接成功
|
// 在主线程通知连接成功
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
if (callback != null) {
|
for (WebSocketCallback callback : mCallbackSet) {
|
||||||
callback.onConnected();
|
callback.onConnected("连接成功");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,7 +307,7 @@ public class WebSocketManager {
|
|||||||
|
|
||||||
// 在主线程通知消息接收
|
// 在主线程通知消息接收
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
if (callback != null) {
|
for (WebSocketCallback callback : mCallbackSet) {
|
||||||
callback.onMessage(text);
|
callback.onMessage(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -305,7 +319,7 @@ public class WebSocketManager {
|
|||||||
|
|
||||||
// 在主线程通知消息接收
|
// 在主线程通知消息接收
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
if (callback != null) {
|
for (WebSocketCallback callback : mCallbackSet) {
|
||||||
callback.onMessage(bytes);
|
callback.onMessage(bytes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -338,7 +352,7 @@ public class WebSocketManager {
|
|||||||
synchronized (connectionLock) {
|
synchronized (connectionLock) {
|
||||||
while (!messageQueue.isEmpty()) {
|
while (!messageQueue.isEmpty()) {
|
||||||
String message = messageQueue.poll();
|
String message = messageQueue.poll();
|
||||||
if (message != null && isConnected && webSocket != null) {
|
if (message != null && mConnected && webSocket != null) {
|
||||||
boolean sent = webSocket.send(message);
|
boolean sent = webSocket.send(message);
|
||||||
if (sent) {
|
if (sent) {
|
||||||
Log.d(TAG, "队列消息发送成功: " + message);
|
Log.d(TAG, "队列消息发送成功: " + message);
|
||||||
@@ -382,8 +396,8 @@ public class WebSocketManager {
|
|||||||
*/
|
*/
|
||||||
private void notifyError(String error) {
|
private void notifyError(String error) {
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
if (callback != null) {
|
for (WebSocketCallback callback : mCallbackSet) {
|
||||||
callback.onError(error);
|
callback.onError("检查网络");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -393,8 +407,8 @@ public class WebSocketManager {
|
|||||||
*/
|
*/
|
||||||
private void notifyDisconnected(String reason) {
|
private void notifyDisconnected(String reason) {
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
if (callback != null) {
|
for (WebSocketCallback callback : mCallbackSet) {
|
||||||
callback.onDisconnected(reason);
|
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;
|
package com.ttstd.remoteservice.service;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
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.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
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 TAG = "ScreenCaptureService2";
|
||||||
|
|
||||||
private static final String CHANNEL_ID = "ScreenCaptureChannel";
|
private static final String CHANNEL_ID = "ScreenCaptureChannel";
|
||||||
private static final int NOTIFICATION_ID = 1001;
|
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
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
Log.e(TAG, "onCreate: ");
|
Log.e(TAG, "onCreate: ");
|
||||||
createNotificationChannel();
|
createNotificationChannel();
|
||||||
if (Build.VERSION.SDK_INT >= 31) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
// Android 12+ 必须指定前台服务类型
|
|
||||||
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
||||||
} else {
|
} else {
|
||||||
// 低版本直接启动
|
|
||||||
startForeground(NOTIFICATION_ID, createNotification());
|
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
|
@Nullable
|
||||||
@@ -39,11 +684,6 @@ public class ScreenCaptureService2 extends Service {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
return START_STICKY; // 服务被杀死后尝试重启
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNotificationChannel() {
|
private void createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
NotificationChannel channel = new NotificationChannel(
|
NotificationChannel channel = new NotificationChannel(
|
||||||
@@ -64,4 +704,117 @@ public class ScreenCaptureService2 extends Service {
|
|||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.build();
|
.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.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.ttstd.remoteservice.BuildConfig;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -18,6 +20,9 @@ public class SystemUtils {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
public static String getSerial() {
|
public static String getSerial() {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
return "981964879";
|
||||||
|
}
|
||||||
String serial = "unknow";
|
String serial = "unknow";
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
|
||||||
|
|||||||
Reference in New Issue
Block a user