feat: 增加预览并远程显示画面
This commit is contained in:
@@ -2,6 +2,14 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def releaseTime() {
|
||||
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
def appName() {
|
||||
return "WebRTCControlled"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.tt.controller'
|
||||
compileSdk 34
|
||||
@@ -14,17 +22,98 @@ android {
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
buildConfig true
|
||||
aidl true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
keypub {
|
||||
storeFile file(rootProject.ext.signingConfigs.keypub.storeFile)
|
||||
storePassword rootProject.ext.signingConfigs.keypub.storePassword
|
||||
keyAlias rootProject.ext.signingConfigs.keypub.keyAlias
|
||||
keyPassword rootProject.ext.signingConfigs.keypub.keyPassword
|
||||
}
|
||||
|
||||
crosshatch {
|
||||
storeFile file(rootProject.ext.signingConfigs.crosshatch.storeFile)
|
||||
storePassword rootProject.ext.signingConfigs.crosshatch.storePassword
|
||||
keyAlias rootProject.ext.signingConfigs.crosshatch.keyAlias
|
||||
keyPassword rootProject.ext.signingConfigs.crosshatch.keyPassword
|
||||
}
|
||||
|
||||
zhanxun {
|
||||
storeFile file(rootProject.ext.signingConfigs.zhanxun.storeFile)
|
||||
storePassword rootProject.ext.signingConfigs.zhanxun.storePassword
|
||||
keyAlias rootProject.ext.signingConfigs.zhanxun.keyAlias
|
||||
keyPassword rootProject.ext.signingConfigs.zhanxun.keyPassword
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
zhanRuiDebug.initWith(debug)
|
||||
zhanRuiDebug {
|
||||
debuggable true
|
||||
signingConfig signingConfigs.zhanxun
|
||||
}
|
||||
|
||||
zhanRuiRelease.initWith(release)
|
||||
zhanRuiRelease {
|
||||
signingConfig signingConfigs.zhanxun
|
||||
}
|
||||
|
||||
CrosshatchDebug.initWith(debug)
|
||||
CrosshatchDebug {
|
||||
debuggable true
|
||||
signingConfig signingConfigs.crosshatch
|
||||
}
|
||||
|
||||
CrosshatchRelease.initWith(release)
|
||||
CrosshatchRelease {
|
||||
signingConfig signingConfigs.crosshatch
|
||||
}
|
||||
|
||||
debug {
|
||||
versionNameSuffix "_debug"
|
||||
debuggable true
|
||||
minifyEnabled false
|
||||
//Zipalign优化
|
||||
zipAlignEnabled true
|
||||
signingConfig signingConfigs.keypub
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled false
|
||||
//Zipalign优化
|
||||
zipAlignEnabled true
|
||||
//前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
//签名
|
||||
signingConfig signingConfigs.keypub
|
||||
}
|
||||
}
|
||||
|
||||
// Add application variant configuration here instead
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def buildType = variant.buildType.name
|
||||
def fileName = ""
|
||||
|
||||
if (buildType.contains("debug")) {
|
||||
fileName = "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk"
|
||||
} else {
|
||||
fileName = "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName = fileName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:sharedUserId="android.uid.system">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -8,13 +9,14 @@
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape">
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.tt.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;
|
||||
@@ -21,8 +23,10 @@ import com.tt.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 {
|
||||
@@ -40,6 +44,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private FrameLayout controlPanel;
|
||||
private LinearLayout setupPanel;
|
||||
private TextView tvStatusControl;
|
||||
private TextView tvStats;
|
||||
|
||||
private WebSocketClient wsClient;
|
||||
private WebRtcClient webRtcClient;
|
||||
@@ -48,6 +53,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
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);
|
||||
@@ -64,9 +78,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
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.1.100:8080/ws/signal");
|
||||
etServerUrl.setText("ws://192.168.100.222:8080/ws/signal");
|
||||
// 生成设备ID
|
||||
myDeviceId = "controller_" + UUID.randomUUID().toString().substring(0, 8);
|
||||
etDeviceId.setText(myDeviceId);
|
||||
@@ -77,6 +92,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
// 初始化 EGL 和远端视频渲染
|
||||
eglBase = EglBase.create();
|
||||
remoteVideoView.init(eglBase.getEglBaseContext(), null);
|
||||
remoteVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
||||
remoteVideoView.setEnableHardwareScaler(true);
|
||||
remoteVideoView.setZOrderMediaOverlay(true);
|
||||
|
||||
// 设置触摸事件
|
||||
@@ -255,6 +272,86 @@ public class MainActivity extends AppCompatActivity {
|
||||
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
|
||||
|
||||
@@ -18,6 +18,7 @@ public class PeerObserver implements PeerConnection.Observer {
|
||||
void onDataChannel(DataChannel dataChannel);
|
||||
void onAddStream(MediaStream stream);
|
||||
void onRemoveStream(MediaStream stream);
|
||||
void onTrack(RtpReceiver receiver, MediaStream[] streams);
|
||||
}
|
||||
|
||||
public PeerObserver(PeerEventListener listener) {
|
||||
@@ -73,5 +74,8 @@ public class PeerObserver implements PeerConnection.Observer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddTrack(RtpReceiver receiver, MediaStream[] streams) {}
|
||||
public void onAddTrack(RtpReceiver receiver, MediaStream[] streams) {
|
||||
android.util.Log.d(TAG, "Track added: " + receiver.id());
|
||||
if (listener != null) listener.onTrack(receiver, streams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,27 @@ import com.tt.controller.signaling.SignalMessage;
|
||||
import com.tt.controller.signaling.WebSocketClient;
|
||||
|
||||
import org.webrtc.DataChannel;
|
||||
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.MediaStreamTrack;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.RtpReceiver;
|
||||
import org.webrtc.RtpTransceiver;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
import org.webrtc.VideoEncoderFactory;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebRtcClient {
|
||||
|
||||
@@ -38,7 +46,8 @@ public class WebRtcClient {
|
||||
private DataChannel dataChannel;
|
||||
private EglBase eglBase;
|
||||
private String currentControlledId;
|
||||
private VideoRenderer videoRenderer;
|
||||
private SurfaceViewRenderer remoteSurfaceView;
|
||||
private VideoTrack remoteVideoTrack;
|
||||
|
||||
public interface ConnectionListener {
|
||||
void onConnectionEstablished();
|
||||
@@ -66,16 +75,36 @@ public class WebRtcClient {
|
||||
PeerConnectionFactory.initialize(initOptions);
|
||||
|
||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||
|
||||
// 创建视频编解码工厂,以支持硬件加速和各种格式
|
||||
VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(
|
||||
eglBase.getEglBaseContext(), true, true);
|
||||
VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
|
||||
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(options)
|
||||
.setVideoEncoderFactory(encoderFactory)
|
||||
.setVideoDecoderFactory(decoderFactory)
|
||||
.createPeerConnectionFactory();
|
||||
}
|
||||
|
||||
public void createOffer(String controlledId, SurfaceViewRenderer remoteRenderer) {
|
||||
this.currentControlledId = controlledId;
|
||||
|
||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(new ArrayList<>());
|
||||
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||
// 添加 STUN 和 TURN 服务器
|
||||
// 注意:如果是 TURN 服务器,必须使用 turn: 前缀
|
||||
iceServers.add(PeerConnection.IceServer.builder("turn:175.178.213.60:3478").setUsername("fanhuitong").setPassword("Fan19961207..").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer());
|
||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
|
||||
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
||||
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||
config.iceCandidatePoolSize = 10;
|
||||
|
||||
PeerObserver peerObserver = new PeerObserver(new PeerObserver.PeerEventListener() {
|
||||
@Override
|
||||
@@ -102,30 +131,41 @@ public class WebRtcClient {
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream stream) {
|
||||
Log.d(TAG, "onAddStream: " + stream.getId());
|
||||
// Unified Plan 下建议使用 onTrack,但为了兼容性也可以处理
|
||||
if (!stream.videoTracks.isEmpty()) {
|
||||
VideoTrack videoTrack = stream.videoTracks.get(0);
|
||||
if (videoRenderer != null) {
|
||||
videoTrack.addSink(videoRenderer);
|
||||
}
|
||||
setupRemoteVideoTrack(videoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream stream) {}
|
||||
|
||||
@Override
|
||||
public void onTrack(RtpReceiver receiver, MediaStream[] streams) {
|
||||
MediaStreamTrack track = receiver.track();
|
||||
if (track instanceof VideoTrack) {
|
||||
Log.d(TAG, "onTrack (video)");
|
||||
setupRemoteVideoTrack((VideoTrack) track);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
||||
|
||||
// 使用 Unified Plan 方式请求视频流 (仅接收)
|
||||
peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO,
|
||||
new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY));
|
||||
|
||||
// 创建 DataChannel
|
||||
DataChannel.Init dcInit = new DataChannel.Init();
|
||||
dcInit.ordered = true;
|
||||
dataChannel = peerConnection.createDataChannel(DATA_CHANNEL_LABEL, dcInit);
|
||||
handleDataChannel(dataChannel);
|
||||
|
||||
// 初始化远端视频渲染器
|
||||
if (remoteRenderer != null) {
|
||||
videoRenderer = new VideoRenderer(remoteRenderer);
|
||||
}
|
||||
// 保存远端视频渲染器
|
||||
this.remoteSurfaceView = remoteRenderer;
|
||||
|
||||
// 创建 Offer
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
@@ -193,6 +233,9 @@ public class WebRtcClient {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (remoteVideoTrack != null && remoteSurfaceView != null) {
|
||||
remoteVideoTrack.removeSink(remoteSurfaceView);
|
||||
}
|
||||
if (dataChannel != null) {
|
||||
dataChannel.close();
|
||||
dataChannel.dispose();
|
||||
@@ -206,6 +249,15 @@ public class WebRtcClient {
|
||||
}
|
||||
}
|
||||
|
||||
private void setupRemoteVideoTrack(VideoTrack videoTrack) {
|
||||
if (remoteSurfaceView != null) {
|
||||
this.remoteVideoTrack = videoTrack;
|
||||
remoteVideoTrack.setEnabled(true);
|
||||
remoteVideoTrack.addSink(remoteSurfaceView);
|
||||
Log.d(TAG, "Remote video track attached to renderer");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDataChannel(DataChannel dc) {
|
||||
dc.registerObserver(new DataChannel.Observer() {
|
||||
@Override
|
||||
@@ -255,4 +307,10 @@ public class WebRtcClient {
|
||||
|
||||
wsClient.sendMessage(msg);
|
||||
}
|
||||
|
||||
public void getStats(org.webrtc.RTCStatsCollectorCallback callback) {
|
||||
if (peerConnection != null) {
|
||||
peerConnection.getStats(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,82 +8,83 @@
|
||||
android:id="@+id/setup_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:gravity="center_vertical">
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="WebRTC 控制端"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="信令服务器地址:"
|
||||
android:textSize="14sp"/>
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_server_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:hint="ws://192.168.1.100:8080/ws/signal"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="ws://192.168.100.222:8080/ws/signal"
|
||||
android:inputType="textUri" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="本机设备ID:"
|
||||
android:textSize="14sp"/>
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="设备ID"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
android:inputType="text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="目标被控设备ID:"
|
||||
android:textSize="14sp"/>
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_target_device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="被控端设备ID"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
android:inputType="text"
|
||||
android:text="981964879" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="状态: 已停止"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_connect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="连接被控设备"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="连接被控设备" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_disconnect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="断开连接"
|
||||
android:enabled="false"/>
|
||||
android:enabled="false"
|
||||
android:text="断开连接" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -98,26 +99,41 @@
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/remote_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- 触摸控制层(覆盖在视频上方) -->
|
||||
<com.tt.controller.view.RemoteTouchView
|
||||
android:id="@+id/touch_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"/>
|
||||
android:background="@android:color/transparent" />
|
||||
|
||||
<!-- 顶部状态栏 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_status_control"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:padding="8dp"
|
||||
android:background="#80000000"
|
||||
android:textColor="@android:color/white"
|
||||
android:text="远程控制中"
|
||||
android:textSize="14sp"/>
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status_control"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="远程控制中"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_stats"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="分辨率: - 帧率: - 延迟: -"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
Reference in New Issue
Block a user