build: rename package

This commit is contained in:
2026-07-14 01:18:55 +08:00
parent 6af1f945ad
commit d14862910a
10 changed files with 17 additions and 17 deletions

View File

@@ -0,0 +1,442 @@
package com.ttstd.controller;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.ttstd.controller.signaling.SignalMessage;
import com.ttstd.controller.signaling.WebSocketClient;
import com.ttstd.controller.view.RemoteTouchView;
import com.ttstd.controller.webrtc.WebRtcClient;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer;
import java.util.Locale;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "ControllerMain";
private EditText etServerUrl;
private EditText etDeviceId;
private EditText etTargetDeviceId;
private Button btnConnect;
private Button btnDisconnect;
private TextView tvStatus;
private SurfaceViewRenderer remoteVideoView;
private RemoteTouchView touchOverlay;
private FrameLayout controlPanel;
private LinearLayout setupPanel;
private TextView tvStatusControl;
private TextView tvStats;
private WebSocketClient wsClient;
private WebRtcClient webRtcClient;
private EglBase eglBase;
private final Gson gson = new Gson();
private String myDeviceId;
private String targetDeviceId;
private final Handler statsHandler = new Handler(Looper.getMainLooper());
private final Runnable statsRunnable = new Runnable() {
@Override
public void run() {
updateStats();
statsHandler.postDelayed(this, 1000);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etServerUrl = findViewById(R.id.et_server_url);
etDeviceId = findViewById(R.id.et_device_id);
etTargetDeviceId = findViewById(R.id.et_target_device_id);
btnConnect = findViewById(R.id.btn_connect);
btnDisconnect = findViewById(R.id.btn_disconnect);
tvStatus = findViewById(R.id.tv_status);
remoteVideoView = findViewById(R.id.remote_video_view);
touchOverlay = findViewById(R.id.touch_overlay);
controlPanel = findViewById(R.id.control_panel);
setupPanel = findViewById(R.id.setup_panel);
tvStatusControl = findViewById(R.id.tv_status_control);
tvStats = findViewById(R.id.tv_stats);
// 默认服务器地址
etServerUrl.setText("ws://192.168.100.222:8080/ws/signal");
// 生成设备ID
myDeviceId = "controller_" + UUID.randomUUID().toString().substring(0, 8);
etDeviceId.setText(myDeviceId);
btnConnect.setOnClickListener(v -> connectToControlled());
btnDisconnect.setOnClickListener(v -> disconnect());
// 初始化 EGL 和远端视频渲染
eglBase = EglBase.create();
remoteVideoView.init(eglBase.getEglBaseContext(), new RendererCommon.RendererEvents() {
@Override
public void onFirstFrameRendered() {
Log.d(TAG, "First frame rendered");
}
@Override
public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) {
Log.d(TAG, "Video resolution changed: " + videoWidth + "x" + videoHeight + " rotation: " + rotation);
adjustVideoSize(videoWidth, videoHeight, rotation);
}
});
remoteVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
remoteVideoView.setEnableHardwareScaler(true);
remoteVideoView.setZOrderMediaOverlay(true);
// 设置触摸事件
touchOverlay.setTouchEventListener(new RemoteTouchView.TouchEventListener() {
@Override
public void onTouch(float relX, float relY) {
sendTouchCommand(relX, relY);
}
@Override
public void onSwipe(float relX1, float relY1, float relX2, float relY2, long duration) {
sendSwipeCommand(relX1, relY1, relX2, relY2, duration);
}
@Override
public void onKeyEvent(int keyCode, int action) {
sendKeyCommand(keyCode, action);
}
});
updateUI(false);
}
private void adjustVideoSize(int videoWidth, int videoHeight, int rotation) {
runOnUiThread(() -> {
if (videoWidth == 0 || videoHeight == 0) return;
int containerWidth = controlPanel.getWidth();
int containerHeight = controlPanel.getHeight();
if (containerWidth == 0 || containerHeight == 0) {
// 如果容器还没测量好,稍后再试
controlPanel.postDelayed(() -> adjustVideoSize(videoWidth, videoHeight, rotation), 100);
return;
}
float videoAspectRatio;
if (rotation == 90 || rotation == 270) {
videoAspectRatio = (float) videoHeight / videoWidth;
} else {
videoAspectRatio = (float) videoWidth / videoHeight;
}
float containerAspectRatio = (float) containerWidth / containerHeight;
int newWidth, newHeight;
if (videoAspectRatio > containerAspectRatio) {
// 视频更宽,以容器宽度为准
newWidth = containerWidth;
newHeight = (int) (containerWidth / videoAspectRatio);
} else {
// 视频更高,以容器高度为准
newWidth = (int) (containerHeight * videoAspectRatio);
newHeight = containerHeight;
}
// 更新视频渲染视图大小
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) remoteVideoView.getLayoutParams();
lp.width = newWidth;
lp.height = newHeight;
remoteVideoView.setLayoutParams(lp);
// 更新触摸层大小,使其与视频区域完全重合
FrameLayout.LayoutParams overlayLp = (FrameLayout.LayoutParams) touchOverlay.getLayoutParams();
overlayLp.width = newWidth;
overlayLp.height = newHeight;
touchOverlay.setLayoutParams(overlayLp);
Log.d(TAG, String.format("Adjusted video view size: %dx%d (video: %dx%d, rotation: %d, container: %dx%d)",
newWidth, newHeight, videoWidth, videoHeight, rotation, containerWidth, containerHeight));
});
}
private void connectToControlled() {
String serverUrl = etServerUrl.getText().toString().trim();
targetDeviceId = etTargetDeviceId.getText().toString().trim();
myDeviceId = etDeviceId.getText().toString().trim();
if (serverUrl.isEmpty() || targetDeviceId.isEmpty() || myDeviceId.isEmpty()) {
Toast.makeText(this, "请填写所有字段", Toast.LENGTH_SHORT).show();
return;
}
tvStatus.setText("状态: 正在连接信令服务器...");
// 初始化 WebSocket
wsClient = new WebSocketClient(serverUrl, myDeviceId);
wsClient.setListener(new WebSocketClient.SignalListener() {
@Override
public void onConnected() {
tvStatus.setText("状态: 已连接信令服务器,正在发起连接...");
// 连接成功后初始化 WebRTC 并创建 Offer
initWebRtcAndConnect();
}
@Override
public void onDisconnected() {
tvStatus.setText("状态: 已断开连接");
updateUI(false);
}
@Override
public void onError(String error) {
tvStatus.setText("状态: 连接错误 - " + error);
updateUI(false);
}
@Override
public void onMessage(SignalMessage message) {
handleSignalMessage(message);
}
});
wsClient.connect();
}
private void initWebRtcAndConnect() {
webRtcClient = new WebRtcClient(this, wsClient, myDeviceId);
webRtcClient.initialize(eglBase);
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
@Override
public void onConnectionEstablished() {
runOnUiThread(() -> {
tvStatus.setText("状态: 已连接 - 远程控制中");
updateUI(true);
touchOverlay.requestFocus();
});
}
@Override
public void onConnectionFailed(String error) {
runOnUiThread(() -> {
tvStatus.setText("状态: 连接失败 - " + error);
updateUI(false);
});
}
@Override
public void onDisconnected() {
runOnUiThread(() -> {
tvStatus.setText("状态: 远端已断开");
updateUI(false);
});
}
});
webRtcClient.createOffer(targetDeviceId, remoteVideoView);
}
private void handleSignalMessage(SignalMessage message) {
String type = message.getType();
if (type == null) return;
switch (type.toUpperCase()) {
case "ANSWER":
handleAnswer(message);
break;
case "ICE_CANDIDATE":
handleIceCandidate(message);
break;
}
}
private void handleAnswer(SignalMessage message) {
try {
JsonObject payload = gson.fromJson(message.getPayload(), JsonObject.class);
String sdp = payload.get("sdp").getAsString();
if (webRtcClient != null) {
webRtcClient.handleAnswer(sdp);
}
} catch (Exception e) {
Log.e(TAG, "Error parsing answer", e);
}
}
private void handleIceCandidate(SignalMessage message) {
try {
JsonObject payload = gson.fromJson(message.getPayload(), JsonObject.class);
String sdpMid = payload.get("sdpMid").getAsString();
int sdpMLineIndex = payload.get("sdpMLineIndex").getAsInt();
String candidate = payload.get("candidate").getAsString();
IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate);
if (webRtcClient != null) {
webRtcClient.addIceCandidate(iceCandidate);
}
} catch (Exception e) {
Log.e(TAG, "Error parsing ICE candidate", e);
}
}
private void sendTouchCommand(float relX, float relY) {
if (webRtcClient != null) {
JsonObject cmd = new JsonObject();
cmd.addProperty("action", "TOUCH");
cmd.addProperty("x", relX);
cmd.addProperty("y", relY);
webRtcClient.sendControlCommand(cmd.toString());
}
}
private void sendSwipeCommand(float relX1, float relY1, float relX2, float relY2, long duration) {
if (webRtcClient != null) {
JsonObject cmd = new JsonObject();
cmd.addProperty("action", "SWIPE");
cmd.addProperty("x1", relX1);
cmd.addProperty("y1", relY1);
cmd.addProperty("x2", relX2);
cmd.addProperty("y2", relY2);
cmd.addProperty("duration", duration);
webRtcClient.sendControlCommand(cmd.toString());
}
}
private void sendKeyCommand(int keyCode, int action) {
if (webRtcClient != null) {
JsonObject cmd = new JsonObject();
cmd.addProperty("action", "KEY");
cmd.addProperty("keyCode", keyCode);
cmd.addProperty("keyAction", action);
webRtcClient.sendControlCommand(cmd.toString());
}
}
private void disconnect() {
if (webRtcClient != null) {
webRtcClient.close();
webRtcClient = null;
}
if (wsClient != null) {
wsClient.disconnect();
wsClient = null;
}
remoteVideoView.clearImage();
updateUI(false);
tvStatus.setText("状态: 已停止");
}
private void updateUI(boolean connected) {
setupPanel.setVisibility(connected ? View.GONE : View.VISIBLE);
controlPanel.setVisibility(connected ? View.VISIBLE : View.GONE);
btnConnect.setEnabled(!connected);
btnDisconnect.setEnabled(connected);
if (tvStatusControl != null) {
tvStatusControl.setText(connected ? "远程控制中" : "未连接");
}
if (connected) {
statsHandler.post(statsRunnable);
} else {
statsHandler.removeCallbacks(statsRunnable);
}
}
private void updateStats() {
if (webRtcClient != null) {
webRtcClient.getStats(report -> {
long width = 0, height = 0, fps = 0, delay = 0;
String codecName = "-";
double avgDecodeTimeMs = 0;
for (org.webrtc.RTCStats stats : report.getStatsMap().values()) {
if ("inbound-rtp".equals(stats.getType()) && "video".equals(stats.getMembers().get("kind"))) {
// 分辨率
Object w = stats.getMembers().get("frameWidth");
if (w instanceof Number) width = ((Number) w).longValue();
Object h = stats.getMembers().get("frameHeight");
if (h instanceof Number) height = ((Number) h).longValue();
// 帧率
Object f = stats.getMembers().get("framesPerSecond");
if (f instanceof Number) fps = ((Number) f).longValue();
// 解码耗时 (平均)
Object totalTime = stats.getMembers().get("totalDecodeTime");
Object frames = stats.getMembers().get("framesDecoded");
if (totalTime instanceof Number && frames instanceof Number) {
double t = ((Number) totalTime).doubleValue();
long c = ((Number) frames).longValue();
if (c > 0) {
avgDecodeTimeMs = (t * 1000.0) / c;
}
}
// 解码格式
Object codecId = stats.getMembers().get("codecId");
if (codecId instanceof String) {
org.webrtc.RTCStats codecStats = report.getStatsMap().get(codecId);
if (codecStats != null) {
Object mime = codecStats.getMembers().get("mimeType");
if (mime instanceof String) {
codecName = (String) mime;
if (codecName.startsWith("video/")) {
codecName = codecName.substring(6);
}
}
}
}
// 解码器实现
Object decoder = stats.getMembers().get("decoderImplementation");
if (decoder instanceof String) {
String d = (String) decoder;
codecName = codecName.equals("-") ? d : codecName + " (" + d + ")";
}
}
if ("candidate-pair".equals(stats.getType())) {
Object nominated = stats.getMembers().get("nominated");
if (Boolean.TRUE.equals(nominated)) {
Object rtt = stats.getMembers().get("currentRoundTripTime");
if (rtt instanceof Number) {
delay = (long) (((Number) rtt).doubleValue() * 1000);
}
}
}
}
final String statsText = String.format(Locale.getDefault(),
"分辨率: %dx%d 帧率: %d 延迟: %dms\n解码格式: %s 平均解码耗时: %.1fms",
width, height, fps, delay, codecName, avgDecodeTimeMs);
runOnUiThread(() -> {
if (tvStats != null) {
tvStats.setText(statsText);
}
});
});
}
}
@Override
protected void onDestroy() {
super.onDestroy();
disconnect();
if (eglBase != null) {
eglBase.release();
}
}
}