feat: 交换信令

This commit is contained in:
TongTongStudio
2026-06-23 10:38:41 +08:00
parent 49ba30179f
commit 898fb69aac
12 changed files with 1037 additions and 806 deletions

View File

@@ -43,8 +43,6 @@
android:name=".service.ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
<!-- 指令执行服务 -->
<service android:name=".service.ControlService" />
<service
android:name=".service.ScreenCaptureService2"
android:exported="false"

View File

@@ -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();
}
}

View File

@@ -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, "屏幕采集权限申请失败");
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -10,7 +10,7 @@ public interface WebSocketCallback {
/**
* 连接成功
*/
void onConnected();
void onConnected(String sessionId);
/**
* 连接断开

View File

@@ -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);
}
});
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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+