From b2f848c918adab6c331b9439a0ae157378b797fb Mon Sep 17 00:00:00 2001
From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com>
Date: Tue, 6 Jan 2026 18:42:23 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=A8=E5=B1=8F=E6=92=AD?=
=?UTF-8?q?=E6=94=BE=E5=92=8C=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=EF=BC=9A1.=E6=B7=BB=E5=8A=A0=E9=80=80=E5=87=BA?=
=?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8C=89=E9=92=AE=202.=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E5=85=A8=E5=B1=8F=E6=92=AD=E6=94=BE=E9=BB=91=E5=B1=8F=E9=97=AE?=
=?UTF-8?q?=E9=A2=98=203.=E6=B7=BB=E5=8A=A0=E9=80=80=E5=87=BA=E5=85=A8?=
=?UTF-8?q?=E5=B1=8F=E6=8C=89=E9=92=AE=204.=E4=BF=AE=E5=A4=8D=E9=80=80?=
=?UTF-8?q?=E5=87=BA=E5=85=A8=E5=B1=8F=E5=90=8E=E5=B8=83=E5=B1=80=E6=81=A2?=
=?UTF-8?q?=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../livestreaming/BroadcastActivity.java | 145 ++++-
.../livestreaming/RoomDetailActivity.java | 299 ++++++---
.../livestreaming/SettingsPageActivity.java | 30 +
.../src/main/res/drawable/bg_chat_input.xml | 6 +
.../res/drawable/bg_live_action_circle.xml | 7 +
.../src/main/res/drawable/bg_live_badge.xml | 8 +-
.../main/res/drawable/bg_live_badge_red.xml | 3 +-
.../res/drawable/bg_live_bottom_gradient.xml | 11 +
.../main/res/drawable/bg_live_chat_input.xml | 10 +
.../main/res/drawable/bg_live_follow_btn.xml | 7 +
.../main/res/drawable/bg_live_gift_btn.xml | 11 +
.../main/res/drawable/bg_live_send_btn.xml | 5 +
.../res/drawable/bg_live_streamer_pill.xml | 7 +
.../res/drawable/bg_live_top_gradient.xml | 11 +
.../main/res/layout/activity_broadcast.xml | 17 +-
.../main/res/layout/activity_room_detail.xml | 586 ++++++++++--------
.../res/layout/activity_room_detail_new.xml | 206 +++---
17 files changed, 903 insertions(+), 466 deletions(-)
create mode 100644 android-app/app/src/main/res/drawable/bg_chat_input.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_action_circle.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_bottom_gradient.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_chat_input.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_follow_btn.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_gift_btn.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_send_btn.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_streamer_pill.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_top_gradient.xml
diff --git a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
index 21e55fb2..c935c454 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
@@ -11,7 +11,9 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.View;
+import android.view.ViewGroup;
import android.view.WindowManager;
+import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -69,13 +71,13 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private boolean streamerVerified = false;
private boolean cameraInitialized = false;
- // 推流参数 - 竖屏直播使用9:16分辨率
- // 手机竖屏直播,直接使用竖屏分辨率,不需要交换宽高
+ // 推流参数 - 竖屏直播
+ // 使用 9:16 或接近手机屏幕比例的分辨率
private static final int VIDEO_WIDTH = 720; // 竖屏宽度
private static final int VIDEO_HEIGHT = 1280; // 竖屏高度 (9:16)
- private static final int VIDEO_FPS = 25; // 标准帧率
- private static final int VIDEO_BITRATE = 1500 * 1024; // 1.5Mbps,高清
- private static final int AUDIO_BITRATE = 64 * 1024;
+ private static final int VIDEO_FPS = 30; // 提高帧率
+ private static final int VIDEO_BITRATE = 2000 * 1024; // 2Mbps,更高清
+ private static final int AUDIO_BITRATE = 128 * 1024; // 128kbps 音频
private static final int AUDIO_SAMPLE_RATE = 44100;
// 直播计时
@@ -83,6 +85,10 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private Handler mainHandler = new Handler(Looper.getMainLooper());
private long startTime = 0;
private Runnable timerRunnable;
+
+ // 预览尺寸(用于计算 centerCrop)
+ private int previewWidth = 0;
+ private int previewHeight = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -316,8 +322,12 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
}
private void initCameraInternal() {
+ // 获取屏幕尺寸
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ int screenHeight = getResources().getDisplayMetrics().heightPixels;
+ Log.d(TAG, "屏幕尺寸: " + screenWidth + "x" + screenHeight);
+
// 手机直播使用竖屏分辨率
- // RootEncoder 会自动处理摄像头旋转
int videoWidth = VIDEO_WIDTH; // 720
int videoHeight = VIDEO_HEIGHT; // 1280
Log.d(TAG, "目标视频分辨率: " + videoWidth + "x" + videoHeight + " (竖屏9:16)");
@@ -355,11 +365,46 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
rtmpCamera2 = null;
} else {
// 编码器准备好后,开始预览
- // 使用 CameraHelper.Facing 来指定摄像头
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
- rtmpCamera2.startPreview(facing, videoWidth, videoHeight);
+
+ // 使用摄像头原生支持的分辨率
+ // 先尝试自动分辨率,让库自己选择最佳分辨率
+ boolean previewStarted = false;
+
+ try {
+ // 直接使用自动分辨率,避免分辨率不支持的问题
+ rtmpCamera2.startPreview(facing);
+ Log.d(TAG, "Camera2 预览启动成功(自动分辨率)");
+ previewStarted = true;
+ // 使用默认的 640x480 计算 centerCrop
+ adjustPreviewSize(640, 480);
+ } catch (Exception e) {
+ Log.w(TAG, "自动分辨率预览失败: " + e.getMessage());
+ }
+
+ if (!previewStarted) {
+ // 自动分辨率失败,尝试常见分辨率
+ int[][] resolutions = {
+ {640, 480}, // VGA 4:3
+ {1280, 720}, // HD 16:9
+ {1920, 1080}, // Full HD 16:9
+ };
+
+ for (int[] res : resolutions) {
+ try {
+ rtmpCamera2.startPreview(facing, res[0], res[1]);
+ Log.d(TAG, "Camera2 预览启动成功: " + res[0] + "x" + res[1]);
+ previewStarted = true;
+ adjustPreviewSize(res[0], res[1]);
+ break;
+ } catch (Exception e) {
+ Log.w(TAG, "预览分辨率 " + res[0] + "x" + res[1] + " 不支持: " + e.getMessage());
+ }
+ }
+ }
+
useCamera2 = true;
- Log.d(TAG, "Camera2 + OpenGlView 初始化完成,预览已开始");
+ Log.d(TAG, "Camera2 + OpenGlView 初始化完成");
return;
}
} catch (Exception e) {
@@ -401,10 +446,42 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
return;
}
+ // 使用摄像头原生支持的分辨率
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
- rtmpCamera1.startPreview(facing);
+
+ // 先尝试自动分辨率
+ boolean previewStarted = false;
+
+ try {
+ rtmpCamera1.startPreview(facing);
+ Log.d(TAG, "Camera1 预览启动成功(自动分辨率)");
+ previewStarted = true;
+ adjustPreviewSize(640, 480);
+ } catch (Exception e) {
+ Log.w(TAG, "自动分辨率预览失败: " + e.getMessage());
+ }
+
+ if (!previewStarted) {
+ int[][] resolutions = {
+ {640, 480},
+ {1280, 720},
+ };
+
+ for (int[] res : resolutions) {
+ try {
+ rtmpCamera1.startPreview(facing, res[0], res[1]);
+ Log.d(TAG, "Camera1 预览启动成功: " + res[0] + "x" + res[1]);
+ previewStarted = true;
+ adjustPreviewSize(res[0], res[1]);
+ break;
+ } catch (Exception e) {
+ Log.w(TAG, "预览分辨率 " + res[0] + "x" + res[1] + " 不支持: " + e.getMessage());
+ }
+ }
+ }
+
useCamera2 = false;
- Log.d(TAG, "Camera1 + OpenGlView 初始化完成,预览已开始");
+ Log.d(TAG, "Camera1 + OpenGlView 初始化完成");
return;
} catch (Exception e) {
Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e);
@@ -424,6 +501,52 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private boolean hasCamera() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
+
+ /**
+ * 调整 OpenGlView 尺寸实现 centerCrop 效果
+ * 让预览画面填满屏幕,裁剪多余部分
+ */
+ private void adjustPreviewSize(int cameraWidth, int cameraHeight) {
+ // 获取容器尺寸
+ int containerWidth = binding.previewContainer.getWidth();
+ int containerHeight = binding.previewContainer.getHeight();
+
+ if (containerWidth == 0 || containerHeight == 0) {
+ Log.w(TAG, "容器尺寸为0,延迟调整");
+ mainHandler.postDelayed(() -> adjustPreviewSize(cameraWidth, cameraHeight), 100);
+ return;
+ }
+
+ // 摄像头输出是横向的,竖屏显示时需要交换宽高
+ int previewW = cameraHeight; // 旋转后的宽度
+ int previewH = cameraWidth; // 旋转后的高度
+
+ Log.d(TAG, "容器尺寸: " + containerWidth + "x" + containerHeight);
+ Log.d(TAG, "预览尺寸(旋转后): " + previewW + "x" + previewH);
+
+ // 计算 centerCrop 需要的缩放比例
+ float containerRatio = (float) containerWidth / containerHeight;
+ float previewRatio = (float) previewW / previewH;
+
+ int newWidth, newHeight;
+
+ if (previewRatio > containerRatio) {
+ // 预览更宽,以高度为基准,宽度会超出
+ newHeight = containerHeight;
+ newWidth = (int) (containerHeight * previewRatio);
+ } else {
+ // 预览更高,以宽度为基准,高度会超出
+ newWidth = containerWidth;
+ newHeight = (int) (containerWidth / previewRatio);
+ }
+
+ Log.d(TAG, "调整后的 OpenGlView 尺寸: " + newWidth + "x" + newHeight);
+
+ // 更新 OpenGlView 尺寸
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(newWidth, newHeight);
+ params.gravity = android.view.Gravity.CENTER;
+ binding.openGlView.setLayoutParams(params);
+ }
private void switchCamera() {
isFrontCamera = !isFrontCamera;
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
index 14b52718..5d22ec6d 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
@@ -278,6 +278,7 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 返回按钮
binding.backButton.setOnClickListener(v -> finish());
+ // 退出全屏按钮点击事件
binding.exitFullscreenButton.setOnClickListener(v -> {
if (isFullscreen) {
toggleFullscreen();
@@ -285,16 +286,6 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
finish();
}
});
-
- View.OnClickListener fullscreenOverlayToggle = v -> {
- if (!isFullscreen) return;
- int vis = binding.exitFullscreenButton.getVisibility();
- binding.exitFullscreenButton.setVisibility(vis == View.VISIBLE ? View.GONE : View.VISIBLE);
- };
-
- // PlayerView 往往会自己消费触摸事件,导致 container 的点击回调不触发,所以这里两处都绑定
- binding.playerContainer.setOnClickListener(fullscreenOverlayToggle);
- binding.playerView.setOnClickListener(fullscreenOverlayToggle);
// 全屏按钮
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
@@ -325,14 +316,26 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
* 手机:视频全屏显示(100%高度),UI元素覆盖在视频上
*/
private void adjustVideoPlayerHeight() {
- // 初始化时不做任何调整,等待视频流加载后根据视频宽高比调整
- android.util.Log.d("RoomDetail", "等待视频流加载后根据视频宽高比调整显示方式...");
+ // 初始化视频区域高度为屏幕的45%
+ android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ int screenHeight = displayMetrics.heightPixels;
+ int videoHeight = (int) (screenHeight * 0.45);
+
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = videoHeight;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ binding.playerContainer.setLayoutParams(playerParams);
+
+ android.util.Log.d("RoomDetail", "初始化视频区域高度: " + videoHeight + " (屏幕高度的45%)");
}
/**
* 根据视频流的宽高比调整显示方式
* 手机推流(竖屏视频,高度>宽度):视频全屏显示
- * 电脑推流(横屏视频,宽度>高度):视频显示50%高度
+ * 电脑推流(横屏视频,宽度>高度):视频在中间显示,保持比例
*/
private void adjustVideoDisplayByStreamType(int videoWidth, int videoHeight) {
try {
@@ -343,48 +346,54 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
int screenWidth = displayMetrics.widthPixels;
// 判断视频是竖屏还是横屏
- // 手机推流:视频是竖屏的(高度 > 宽度)
- // 电脑推流:视频是横屏的(宽度 > 高度)
boolean isMobileStream = videoHeight > videoWidth;
android.util.Log.d("RoomDetail", "视频尺寸: " + videoWidth + "x" + videoHeight +
", 屏幕尺寸: " + screenWidth + "x" + screenHeight +
", 推流类型: " + (isMobileStream ? "手机(竖屏)" : "电脑(横屏)"));
- // 无论什么类型的推流,都让视频全屏显示
- // 手机推流的竖屏视频会自动填满屏幕
- // 电脑推流的横屏视频会在上下留黑边(保持比例)
- android.util.Log.d("RoomDetail", "视频全屏显示模式");
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+
+ if (isMobileStream) {
+ // 手机推流(竖屏视频):全屏显示
+ android.util.Log.d("RoomDetail", "手机推流 - 全屏显示模式");
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ } else {
+ // 电脑推流(横屏视频):在中间显示,保持16:9比例
+ android.util.Log.d("RoomDetail", "电脑推流 - 居中显示模式");
+
+ // 计算16:9比例的高度
+ float aspectRatio = (float) videoWidth / videoHeight;
+ int targetHeight = (int) (screenWidth / aspectRatio);
+
+ // 限制最大高度为屏幕的60%
+ int maxHeight = (int) (screenHeight * 0.6);
+ if (targetHeight > maxHeight) {
+ targetHeight = maxHeight;
+ }
+
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = targetHeight;
+ playerParams.gravity = android.view.Gravity.CENTER;
+
+ android.util.Log.d("RoomDetail", "视频区域高度: " + targetHeight + " (屏幕高度的" +
+ (targetHeight * 100 / screenHeight) + "%)");
+ }
- // 设置 playerContainer 全屏
- android.view.ViewGroup.LayoutParams playerParams = binding.playerContainer.getLayoutParams();
- playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
- playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
binding.playerContainer.setLayoutParams(playerParams);
- // 设置 SurfaceView 全屏填充
+ // 设置 SurfaceView
if (binding.flvSurfaceView != null && binding.flvSurfaceView.getVisibility() == View.VISIBLE) {
android.view.ViewGroup.LayoutParams surfaceParams = binding.flvSurfaceView.getLayoutParams();
surfaceParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
surfaceParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
binding.flvSurfaceView.setLayoutParams(surfaceParams);
-
- // 关键:设置 SurfaceView 的固定尺寸以保持视频比例
- // 对于竖屏视频,让它填满屏幕宽度,高度按比例计算
- if (isMobileStream) {
- // 竖屏视频:宽度填满屏幕,高度按比例
- float aspectRatio = (float) videoHeight / videoWidth;
- int targetHeight = (int) (screenWidth * aspectRatio);
- if (targetHeight > screenHeight) {
- // 如果计算出的高度超过屏幕,则以屏幕高度为准
- targetHeight = screenHeight;
- }
- binding.flvSurfaceView.getHolder().setFixedSize(screenWidth, targetHeight);
- android.util.Log.d("RoomDetail", "SurfaceView 固定尺寸: " + screenWidth + "x" + targetHeight);
- }
}
- // 设置 PlayerView 全屏填充
+ // 设置 PlayerView
if (binding.playerView != null) {
android.view.ViewGroup.LayoutParams pvParams = binding.playerView.getLayoutParams();
pvParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -624,28 +633,42 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 停止之前的重连任务
stopChatReconnect();
+ // 获取用户token
+ String token = AuthStore.getToken(this);
+
+ // 构建WebSocket URL,添加token参数
String wsUrl = getWsChatBaseUrl() + roomId;
+ if (!TextUtils.isEmpty(token)) {
+ wsUrl += "?token=" + token;
+ }
android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl);
- chatWsClient = new OkHttpClient.Builder()
- .pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
- .build();
- Request request = new Request.Builder()
- .url(wsUrl)
- .build();
-
- chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
- @Override
- public void onOpen(WebSocket webSocket, okhttp3.Response response) {
- android.util.Log.d("ChatWebSocket", "弹幕连接成功: roomId=" + roomId);
- isChatWebSocketConnected = true;
- chatReconnectAttempts = 0;
- // 启动心跳检测
- startChatHeartbeat();
- }
+ try {
+ chatWsClient = new OkHttpClient.Builder()
+ .pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
+ .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
+ .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
+ .build();
+ Request request = new Request.Builder()
+ .url(wsUrl)
+ .build();
- @Override
- public void onMessage(WebSocket webSocket, String text) {
+ chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
+ @Override
+ public void onOpen(WebSocket webSocket, okhttp3.Response response) {
+ android.util.Log.d("ChatWebSocket", "弹幕连接成功: roomId=" + roomId);
+ isChatWebSocketConnected = true;
+ chatReconnectAttempts = 0;
+ // 启动心跳检测
+ startChatHeartbeat();
+
+ handler.post(() -> {
+ addChatMessage(new ChatMessage("已连接到弹幕服务", true));
+ });
+ }
+
+ @Override
+ public void onMessage(WebSocket webSocket, String text) {
// 收到消息,解析并显示
android.util.Log.d("ChatWebSocket", "收到消息: " + text);
try {
@@ -709,9 +732,19 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
@Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
- android.util.Log.e("ChatWebSocket", "连接失败: " + t.getMessage());
+ String errorMsg = t != null ? t.getMessage() : "未知错误";
+ int responseCode = response != null ? response.code() : -1;
+ android.util.Log.e("ChatWebSocket", "连接失败: " + errorMsg + ", responseCode=" + responseCode);
isChatWebSocketConnected = false;
stopChatHeartbeat();
+
+ // 在UI上显示连接状态(仅首次失败时显示)
+ if (chatReconnectAttempts == 0) {
+ handler.post(() -> {
+ addChatMessage(new ChatMessage("弹幕服务连接中...", true));
+ });
+ }
+
// 尝试重连
scheduleChatReconnect();
}
@@ -727,6 +760,11 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
}
}
});
+ } catch (Exception e) {
+ android.util.Log.e("ChatWebSocket", "创建WebSocket失败: " + e.getMessage());
+ isChatWebSocketConnected = false;
+ scheduleChatReconnect();
+ }
}
/**
@@ -949,10 +987,17 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
}
private void sendChatViaWebSocket(String content) {
+ // 先本地显示消息(乐观更新)
+ String nickname = AuthStore.getNickname(this);
+ if (TextUtils.isEmpty(nickname)) {
+ nickname = "我";
+ }
+ addChatMessage(new ChatMessage(nickname, content));
+
if (chatWebSocket == null || !isChatWebSocketConnected) {
- // 如果 WebSocket 未连接,先本地显示
- addChatMessage(new ChatMessage("我", content));
- Toast.makeText(this, "弹幕连接断开,消息仅本地显示", Toast.LENGTH_SHORT).show();
+ // WebSocket 未连接,尝试重连
+ android.util.Log.w("ChatWebSocket", "WebSocket未连接,尝试重连...");
+ connectChatWebSocket();
return;
}
@@ -960,14 +1005,15 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
JSONObject json = new JSONObject();
json.put("type", "chat");
json.put("content", content);
- json.put("nickname", AuthStore.getNickname(this));
+ json.put("nickname", nickname);
json.put("userId", AuthStore.getUserId(this));
- chatWebSocket.send(json.toString());
+ boolean sent = chatWebSocket.send(json.toString());
+ if (!sent) {
+ android.util.Log.w("ChatWebSocket", "消息发送失败,WebSocket可能已断开");
+ }
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送消息失败: " + e.getMessage());
- // 失败时本地显示
- addChatMessage(new ChatMessage("我", content));
}
}
@@ -1020,25 +1066,63 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 退出全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- // ActionBar可能为null,需要检查
- androidx.appcompat.app.ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.show();
- }
+ // 恢复系统UI
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
isFullscreen = false;
- if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
+ if (binding != null) {
+ binding.exitFullscreenButton.setVisibility(View.GONE);
+ binding.fullscreenButton.setVisibility(View.VISIBLE);
+
+ // 恢复视频区域大小 - 使用屏幕高度的50%
+ android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ int screenHeight = displayMetrics.heightPixels;
+ int videoHeight = (int) (screenHeight * 0.45); // 45%屏幕高度
+
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = videoHeight;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ playerParams.setMargins(0, 0, 0, 0);
+ binding.playerContainer.setLayoutParams(playerParams);
+
+ // 显示所有UI元素
+ binding.uiOverlayContainer.setVisibility(View.VISIBLE);
+ }
} else {
- // 进入全屏
+ // 进入全屏(横屏)
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
- // ActionBar可能为null,需要检查
- androidx.appcompat.app.ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.hide();
- }
+ // 隐藏系统UI(状态栏和导航栏)
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
isFullscreen = true;
- if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
+ if (binding != null) {
+ binding.exitFullscreenButton.setVisibility(View.VISIBLE);
+ binding.fullscreenButton.setVisibility(View.GONE);
+
+ // 全屏时视频区域填满屏幕
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ playerParams.setMargins(0, 0, 0, 0);
+ binding.playerContainer.setLayoutParams(playerParams);
+
+ // 隐藏UI覆盖层(除了退出按钮)
+ binding.uiOverlayContainer.setVisibility(View.GONE);
+ }
}
}
@@ -1065,18 +1149,59 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
- // 横屏时隐藏其他UI元素,只显示播放器
- binding.topBar.setVisibility(View.GONE);
- binding.roomInfoLayout.setVisibility(View.GONE);
- binding.chatInputLayout.setVisibility(View.GONE);
- binding.chatRecyclerView.setVisibility(View.GONE);
+ // 横屏时隐藏其他UI元素,只显示播放器和退出按钮
+ isFullscreen = true;
+
+ // 隐藏系统UI
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
+ // 隐藏UI覆盖层
+ binding.uiOverlayContainer.setVisibility(View.GONE);
+
+ // 全屏时视频区域填满屏幕
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ playerParams.setMargins(0, 0, 0, 0);
+ binding.playerContainer.setLayoutParams(playerParams);
+
+ // 显示退出全屏按钮(在playerContainer内)
binding.exitFullscreenButton.setVisibility(View.VISIBLE);
+ binding.fullscreenButton.setVisibility(View.GONE);
} else {
// 竖屏时显示所有UI元素
- binding.topBar.setVisibility(View.VISIBLE);
- binding.roomInfoLayout.setVisibility(View.VISIBLE);
- binding.chatInputLayout.setVisibility(View.VISIBLE);
- binding.chatRecyclerView.setVisibility(View.VISIBLE);
+ isFullscreen = false;
+
+ // 恢复系统UI
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
+ // 显示UI覆盖层
+ binding.uiOverlayContainer.setVisibility(View.VISIBLE);
+
+ // 竖屏时恢复视频区域大小 - 使用屏幕高度的45%
+ android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ int screenHeight = displayMetrics.heightPixels;
+ int videoHeight = (int) (screenHeight * 0.45);
+
+ android.widget.FrameLayout.LayoutParams playerParams =
+ (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
+ playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+ playerParams.height = videoHeight;
+ playerParams.gravity = android.view.Gravity.CENTER;
+ playerParams.setMargins(0, 0, 0, 0);
+ binding.playerContainer.setLayoutParams(playerParams);
+
+ binding.fullscreenButton.setVisibility(View.VISIBLE);
binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
index 501970bc..c0a31a88 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
@@ -133,6 +133,11 @@ public class SettingsPageActivity extends AppCompatActivity {
startActivity(intent);
return;
}
+
+ if ("退出登录".equals(t)) {
+ showLogoutDialog();
+ return;
+ }
// 处理其他页面的点击事件
if (PAGE_SERVER.equals(page)) {
@@ -385,6 +390,12 @@ public class SettingsPageActivity extends AppCompatActivity {
list.add(MoreItem.row("申请成为主播", "认证后可开播", R.drawable.ic_live_24));
}
+ // 退出登录(仅在已登录时显示)
+ if (com.example.livestreaming.net.AuthStore.getToken(this) != null) {
+ list.add(MoreItem.section("账号"));
+ list.add(MoreItem.row("退出登录", "退出当前账号", R.drawable.ic_person_24));
+ }
+
return list;
}
@@ -851,6 +862,25 @@ public class SettingsPageActivity extends AppCompatActivity {
.show();
}
+ private void showLogoutDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("退出登录")
+ .setMessage("确定要退出当前账号吗?")
+ .setPositiveButton("确定", (d, w) -> {
+ // 清除所有登录数据
+ com.example.livestreaming.net.AuthStore.clearAll(this);
+ Toast.makeText(this, "已退出登录", Toast.LENGTH_SHORT).show();
+
+ // 跳转到登录页面并清除任务栈
+ Intent intent = new Intent(this, LoginActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
private int dp(int value) {
return (int) (value * getResources().getDisplayMetrics().density);
}
diff --git a/android-app/app/src/main/res/drawable/bg_chat_input.xml b/android-app/app/src/main/res/drawable/bg_chat_input.xml
new file mode 100644
index 00000000..7a327bee
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_chat_input.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_action_circle.xml b/android-app/app/src/main/res/drawable/bg_live_action_circle.xml
new file mode 100644
index 00000000..dc22e755
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_action_circle.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_badge.xml b/android-app/app/src/main/res/drawable/bg_live_badge.xml
index 8b7263c6..8e58c00c 100644
--- a/android-app/app/src/main/res/drawable/bg_live_badge.xml
+++ b/android-app/app/src/main/res/drawable/bg_live_badge.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
-
diff --git a/android-app/app/src/main/res/drawable/bg_live_badge_red.xml b/android-app/app/src/main/res/drawable/bg_live_badge_red.xml
index 2b2f1262..1e58cc19 100644
--- a/android-app/app/src/main/res/drawable/bg_live_badge_red.xml
+++ b/android-app/app/src/main/res/drawable/bg_live_badge_red.xml
@@ -1,6 +1,7 @@
+
-
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_bottom_gradient.xml b/android-app/app/src/main/res/drawable/bg_live_bottom_gradient.xml
new file mode 100644
index 00000000..11cc1599
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_bottom_gradient.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_chat_input.xml b/android-app/app/src/main/res/drawable/bg_live_chat_input.xml
new file mode 100644
index 00000000..b54f29fa
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_chat_input.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_follow_btn.xml b/android-app/app/src/main/res/drawable/bg_live_follow_btn.xml
new file mode 100644
index 00000000..25e62479
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_follow_btn.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_gift_btn.xml b/android-app/app/src/main/res/drawable/bg_live_gift_btn.xml
new file mode 100644
index 00000000..796457d4
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_gift_btn.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_send_btn.xml b/android-app/app/src/main/res/drawable/bg_live_send_btn.xml
new file mode 100644
index 00000000..aa99c8bc
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_send_btn.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_streamer_pill.xml b/android-app/app/src/main/res/drawable/bg_live_streamer_pill.xml
new file mode 100644
index 00000000..f11aa07e
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_streamer_pill.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_top_gradient.xml b/android-app/app/src/main/res/drawable/bg_live_top_gradient.xml
new file mode 100644
index 00000000..2c3c0859
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_top_gradient.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_broadcast.xml b/android-app/app/src/main/res/layout/activity_broadcast.xml
index b1a234d0..86060875 100644
--- a/android-app/app/src/main/res/layout/activity_broadcast.xml
+++ b/android-app/app/src/main/res/layout/activity_broadcast.xml
@@ -5,16 +5,23 @@
android:layout_height="match_parent"
android:background="#000000">
-
-
+
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+ android:layout_height="match_parent"
+ android:background="#000000">
-
-
-
+
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+ android:layout_marginStart="8dp"
+ android:layout_weight="1"
+ android:orientation="vertical">
-
-
-
-
-
-
-
-
-
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:text="主播名称"
+ android:textColor="@android:color/white"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_weight="1"
+ android:ellipsize="middle"
+ android:singleLine="true"
+ android:text="rtmp://..."
+ android:textColor="#CCCCCC"
+ android:textSize="12sp" />
-
-
-
-
-
-
+ android:background="?attr/selectableItemBackground"
+ android:padding="4dp"
+ android:text="复制"
+ android:textColor="#4FC3F7"
+ android:textSize="12sp" />
+
-
+
-
+ android:layout_weight="1"
+ android:ellipsize="middle"
+ android:singleLine="true"
+ android:text="密钥: ..."
+ android:textColor="#CCCCCC"
+ android:textSize="12sp" />
-
-
-
-
-
-
-
-
-
+ android:background="?attr/selectableItemBackground"
+ android:padding="4dp"
+ android:text="复制"
+ android:textColor="#4FC3F7"
+ android:textSize="12sp" />
+
-
+
+
-
-
+
+
-
+
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/android-app/app/src/main/res/layout/activity_room_detail_new.xml b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
index 92c36c2e..cc59e714 100644
--- a/android-app/app/src/main/res/layout/activity_room_detail_new.xml
+++ b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
@@ -5,12 +5,12 @@
android:layout_height="match_parent"
android:background="#000000">
-
-
+
@@ -29,18 +29,18 @@
android:layout_gravity="center"
android:visibility="gone" />
-
+
-
+
+
+
+
+
+
+
@@ -124,17 +150,79 @@
android:src="@drawable/ic_arrow_back_24"
app:tint="@android:color/white" />
-
+
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="8dp"
+ android:background="@drawable/bg_rounded_semi_transparent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingHorizontal="8dp"
+ android:paddingVertical="6dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-