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