build: rename package
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user