zhibo/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java

3166 lines
136 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.example.livestreaming;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.GridLayoutManager;
import com.example.livestreaming.call.WebRTCConfig;
import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.ChatMessageResponse;
import com.example.livestreaming.net.CreateRechargeRequest;
import com.example.livestreaming.net.CreateRechargeResponse;
import com.example.livestreaming.net.GiftResponse;
import com.example.livestreaming.net.OrderPayRequest;
import com.example.livestreaming.net.OrderPayResultResponse;
import com.example.livestreaming.net.RechargeOptionResponse;
import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.SendGiftRequest;
import com.example.livestreaming.net.SendGiftResponse;
import com.example.livestreaming.net.StreamConfig;
import com.example.livestreaming.net.UserBalanceResponse;
import com.example.livestreaming.ShareUtils;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
// GSYVideoPlayer imports
import com.shuyu.gsyvideoplayer.GSYVideoManager;
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack;
import com.shuyu.gsyvideoplayer.utils.GSYVideoType;
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer;
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder;
import com.shuyu.gsyvideoplayer.player.PlayerFactory;
import com.shuyu.gsyvideoplayer.player.IjkPlayerManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.json.JSONException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class RoomDetailActivity extends AppCompatActivity implements SurfaceHolder.Callback {
public static final String EXTRA_ROOM_ID = "extra_room_id";
private ActivityRoomDetailNewBinding binding;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable pollRunnable;
private String roomId;
private Room room;
// 观看历史相关
private long enterTime = 0; // 进入直播间的时间戳
private ExoPlayer player;
private boolean triedAltUrl;
private IjkMediaPlayer ijkPlayer;
private Surface ijkSurface;
private SurfaceHolder ijkSurfaceHolder;
private String ijkUrl;
private String ijkFallbackHlsUrl;
private boolean ijkFallbackTried;
private String pendingFlvUrl;
// GSYVideoPlayer 相关
private StandardGSYVideoPlayer gsyPlayer;
private boolean useGSYPlayer = true; // 默认使用GSYVideoPlayer播放FLV
private static boolean ijkLibLoaded;
private boolean isFullscreen = false;
private ChatAdapter chatAdapter;
private List<ChatMessage> chatMessages = new ArrayList<>();
private Random random = new Random();
// WebSocket - 弹幕
private WebSocket chatWebSocket;
private OkHttpClient chatWsClient;
// WebSocket - 在线人数
private WebSocket onlineCountWebSocket;
private OkHttpClient onlineCountWsClient;
// 动态获取WebSocket URL - 直播服务使用远程服务器
private String getWsChatBaseUrl() {
try {
// 直播弹幕WebSocket使用远程服务器
return WebRTCConfig.getLiveWsUrl() + "ws/live/chat/";
} catch (Exception e) {
android.util.Log.e("RoomDetail", "获取WsChatBaseUrl失败", e);
return "ws://1.15.149.240:8083/ws/live/chat/";
}
}
private String getWsOnlineBaseUrl() {
try {
// 直播在线人数WebSocket使用远程服务器
return WebRTCConfig.getLiveWsUrl() + "ws/live/";
} catch (Exception e) {
android.util.Log.e("RoomDetail", "获取WsOnlineBaseUrl失败", e);
return "ws://1.15.149.240:8083/ws/live/";
}
}
// WebSocket 心跳检测 - 弹幕
private Runnable chatHeartbeatRunnable;
private Runnable chatReconnectRunnable;
private int chatReconnectAttempts = 0;
private boolean isChatWebSocketConnected = false;
// WebSocket 心跳检测 - 在线人数
private Runnable onlineHeartbeatRunnable;
private Runnable onlineReconnectRunnable;
private int onlineReconnectAttempts = 0;
private boolean isOnlineWebSocketConnected = false;
// WebSocket 常量
private static final long HEARTBEAT_INTERVAL = 30000; // 30秒心跳间隔
private static final long RECONNECT_DELAY = 5000; // 5秒重连延迟
private static final int MAX_RECONNECT_ATTEMPTS = 5;
// 礼物相关
private BottomSheetDialog giftDialog;
private GiftAdapter giftAdapter;
private List<Gift> availableGifts;
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
private GiftAnimationManager giftAnimationManager; // 礼物动画管理器
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
android.util.Log.d("RoomDetail", "onCreate开始");
// 隐藏ActionBar使用自定义顶部栏
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
// 设置沉浸式全屏模式
setupImmersiveMode();
android.util.Log.d("RoomDetail", "开始inflate布局");
binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
android.util.Log.d("RoomDetail", "布局inflate完成");
ApiClient.getService(getApplicationContext());
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
android.util.Log.d("RoomDetail", "roomId=" + roomId);
if (TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "直播间ID无效", Toast.LENGTH_SHORT).show();
finish();
return;
}
triedAltUrl = false;
android.util.Log.d("RoomDetail", "开始setupUI");
setupUI();
android.util.Log.d("RoomDetail", "setupUI完成开始setupSurface");
setupSurface();
android.util.Log.d("RoomDetail", "setupSurface完成开始setupChat");
setupChat();
android.util.Log.d("RoomDetail", "setupChat完成开始setupGifts");
setupGifts();
android.util.Log.d("RoomDetail", "setupGifts完成");
// 初始化礼物动画管理器
android.util.Log.d("RoomDetail", "=== 初始化礼物动画管理器 ===");
// 从binding的根视图中查找
View giftAnimationOverlay = binding.getRoot().findViewById(R.id.giftAnimationOverlay);
android.util.Log.d("RoomDetail", "giftAnimationOverlay = " + giftAnimationOverlay);
if (giftAnimationOverlay != null) {
giftAnimationManager = new GiftAnimationManager(this, giftAnimationOverlay);
android.util.Log.d("RoomDetail", "✅ 礼物动画管理器初始化成功");
} else {
android.util.Log.e("RoomDetail", "❌ 找不到giftAnimationOverlay布局");
}
// 加载房间信息
android.util.Log.d("RoomDetail", "开始loadRoomInfo");
loadRoomInfo();
android.util.Log.d("RoomDetail", "loadRoomInfo完成");
// 记录观看历史(异步,不阻塞)
recordWatchHistory();
android.util.Log.d("RoomDetail", "onCreate完成");
} catch (Exception e) {
android.util.Log.e("RoomDetail", "onCreate异常: " + e.getMessage(), e);
e.printStackTrace();
Toast.makeText(this, "加载直播间失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
finish();
}
}
/**
* 设置沉浸式全屏模式
*/
private void setupImmersiveMode() {
// 设置全屏沉浸式体验
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
);
// 设置状态栏透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);
}
// 隐藏导航栏(可选)
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
private void setupUI() {
// 调整视频显示高度电脑50%手机100%
adjustVideoPlayerHeight();
// 返回按钮
binding.backButton.setOnClickListener(v -> finish());
binding.exitFullscreenButton.setOnClickListener(v -> {
if (isFullscreen) {
toggleFullscreen();
} else {
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());
// 关注按钮
binding.followButton.setOnClickListener(v -> {
// 检查登录状态,关注主播需要登录
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
return;
}
if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 调用后端接口关注主播
followStreamerBackend();
});
// 分享按钮
binding.shareButton.setOnClickListener(v -> shareRoom());
}
/**
* 根据设备类型调整视频播放器高度
* 电脑/平板视频显示50%高度下方保留UI控件区域
* 手机视频全屏显示100%高度UI元素覆盖在视频上
*/
private void adjustVideoPlayerHeight() {
// 初始化时不做任何调整,等待视频流加载后根据视频宽高比调整
android.util.Log.d("RoomDetail", "等待视频流加载后根据视频宽高比调整显示方式...");
}
/**
* 根据视频流的宽高比调整显示方式
* 手机推流(竖屏视频,高度>宽度):视频全屏显示
* 电脑推流(横屏视频,宽度>高度视频显示50%高度
*/
private void adjustVideoDisplayByStreamType(int videoWidth, int videoHeight) {
try {
// 获取屏幕尺寸
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
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", "视频全屏显示模式");
// 设置 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 全屏填充
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 全屏填充
if (binding.playerView != null) {
android.view.ViewGroup.LayoutParams pvParams = binding.playerView.getLayoutParams();
pvParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
pvParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
binding.playerView.setLayoutParams(pvParams);
}
android.util.Log.d("RoomDetail", "视频显示方式已调整");
} catch (Exception e) {
android.util.Log.e("RoomDetail", "调整视频显示失败: " + e.getMessage(), e);
}
}
/**
* 初始化 SurfaceView 用于 IjkPlayer 播放 FLV
*/
private void setupSurface() {
if (binding.flvSurfaceView != null) {
binding.flvSurfaceView.getHolder().addCallback(this);
android.util.Log.d("RoomDetail", "SurfaceView callback 已设置");
}
}
private void setupChat() {
// 设置弹幕适配器
chatAdapter = new ChatAdapter();
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(true); // 从底部开始显示
binding.chatRecyclerView.setLayoutManager(layoutManager);
binding.chatRecyclerView.setAdapter(chatAdapter);
// 礼物按钮点击事件
binding.giftButton.setOnClickListener(v -> showGiftDialog());
// 发送按钮点击事件
binding.sendButton.setOnClickListener(v -> sendMessage());
// 点赞按钮点击事件
ImageButton likeButton = findViewById(R.id.likeButton);
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeButton != null && likeCountText != null) {
// 加载点赞数
loadLikeCount();
// 点赞按钮点击事件
likeButton.setOnClickListener(v -> {
if (!AuthHelper.isLoggedIn(this)) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return;
}
// 点赞动画
likeButton.animate()
.scaleX(1.3f)
.scaleY(1.3f)
.setDuration(100)
.withEndAction(() -> {
likeButton.animate()
.scaleX(1.0f)
.scaleY(1.0f)
.setDuration(100)
.start();
})
.start();
// 调用点赞API
likeRoom();
});
}
// 输入框回车发送
binding.chatInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND ||
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
sendMessage();
return true;
}
return false;
});
}
/**
* 加载点赞数
*/
private void loadLikeCount() {
if (roomId == null) return;
try {
int roomIdInt = Integer.parseInt(roomId);
ApiClient.getService(this)
.getRoomLikeCount(roomIdInt)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> data = response.body().getData();
if (data != null && data.containsKey("likeCount")) {
int likeCount = ((Number) data.get("likeCount")).intValue();
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
likeCountText.setText(String.valueOf(likeCount));
}
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 忽略错误
}
});
} catch (NumberFormatException e) {
// 忽略错误
}
}
/**
* 点赞直播间
*/
private void likeRoom() {
if (roomId == null) {
android.util.Log.e("RoomDetail", "点赞失败: roomId为空");
Toast.makeText(this, "直播间信息错误", Toast.LENGTH_SHORT).show();
return;
}
try {
int roomIdInt = Integer.parseInt(roomId);
// 立即更新UI先乐观更新
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
try {
int currentCount = Integer.parseInt(likeCountText.getText().toString());
likeCountText.setText(String.valueOf(currentCount + 1));
} catch (NumberFormatException e) {
// 如果解析失败从1开始
likeCountText.setText("1");
}
}
Map<String, Object> request = new HashMap<>();
request.put("count", 1);
android.util.Log.d("RoomDetail", "开始点赞: roomId=" + roomIdInt);
ApiClient.getService(this)
.likeRoom(roomIdInt, request)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
android.util.Log.d("RoomDetail", "点赞响应: code=" + response.code() +
", body=" + (response.body() != null ? response.body().toString() : "null"));
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.isOk()) {
Map<String, Object> data = apiResponse.getData();
if (data != null && data.containsKey("likeCount")) {
// 使用服务器返回的真实点赞数更新UI
int likeCount = ((Number) data.get("likeCount")).intValue();
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
likeCountText.setText(String.valueOf(likeCount));
}
}
// 不显示Toast避免打扰用户
} else {
// 如果失败,恢复原来的点赞数
loadLikeCount();
String errorMsg = apiResponse.getMessage();
android.util.Log.e("RoomDetail", "点赞失败: " + errorMsg);
Toast.makeText(RoomDetailActivity.this,
errorMsg != null ? errorMsg : "点赞失败",
Toast.LENGTH_SHORT).show();
}
} else {
// 如果失败,恢复原来的点赞数
loadLikeCount();
android.util.Log.e("RoomDetail", "点赞请求失败: code=" + response.code());
Toast.makeText(RoomDetailActivity.this, "点赞失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 如果失败,恢复原来的点赞数
loadLikeCount();
android.util.Log.e("RoomDetail", "点赞网络错误: " + t.getMessage(), t);
Toast.makeText(RoomDetailActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
} catch (NumberFormatException e) {
android.util.Log.e("RoomDetail", "直播间ID格式错误: " + roomId);
Toast.makeText(this, "直播间ID格式错误", Toast.LENGTH_SHORT).show();
}
}
private void sendMessage() {
// 检查登录状态,发送弹幕需要登录
if (!AuthHelper.requireLoginWithToast(this, "发送弹幕需要登录")) {
return;
}
String message = binding.chatInput.getText() != null ?
binding.chatInput.getText().toString().trim() : "";
if (!TextUtils.isEmpty(message)) {
binding.chatInput.setText("");
// 通过 WebSocket 发送消息
sendChatViaWebSocket(message);
}
}
private void connectWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 连接弹幕WebSocket
connectChatWebSocket();
// 连接在线人数WebSocket
connectOnlineCountWebSocket();
}
/**
* 连接弹幕WebSocket
*/
private void connectChatWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 停止之前的重连任务
stopChatReconnect();
String wsUrl = getWsChatBaseUrl() + roomId;
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();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 收到消息,解析并显示
android.util.Log.d("ChatWebSocket", "收到消息: " + text);
try {
JSONObject json = new JSONObject(text);
String type = json.optString("type", "");
android.util.Log.d("ChatWebSocket", "消息类型: " + type);
if ("chat".equals(type)) {
String nickname = json.optString("nickname", "匿名");
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(nickname, content));
});
} else if ("gift".equals(type)) {
// 处理礼物消息
String giftName = json.optString("giftName", "");
int count = json.optInt("count", 1);
String senderNickname = json.optString("senderNickname", "匿名");
int totalPrice = json.optInt("totalPrice", 0);
android.util.Log.d("WebSocket", "收到礼物消息: " + json.toString());
handler.post(() -> {
String giftMsg = senderNickname + " 送出了 " + count + "" + giftName;
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
// 显示礼物动画效果
android.util.Log.d("WebSocket", "准备显示礼物动画: " + giftName);
showGiftAnimation(giftName, count, senderNickname);
});
} else if ("gift_combo".equals(type)) {
// 处理礼物连击消息
String giftName = json.optString("giftName", "");
int comboCount = json.optInt("comboCount", 1);
String senderNickname = json.optString("senderNickname", "匿名");
handler.post(() -> {
String comboMsg = senderNickname + "" + giftName + " 连击 x" + comboCount;
addChatMessage(new ChatMessage(comboMsg, true));
});
} else if ("connected".equals(type)) {
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(content, true));
});
} else if ("system".equals(type)) {
// 处理系统消息
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(content, true));
});
} else if ("pong".equals(type)) {
// 收到心跳响应
android.util.Log.d("ChatWebSocket", "收到心跳响应");
}
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "解析消息失败: " + e.getMessage());
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
android.util.Log.e("ChatWebSocket", "连接失败: " + t.getMessage());
isChatWebSocketConnected = false;
stopChatHeartbeat();
// 尝试重连
scheduleChatReconnect();
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
android.util.Log.d("ChatWebSocket", "连接关闭: " + reason);
isChatWebSocketConnected = false;
stopChatHeartbeat();
// 非正常关闭时尝试重连
if (code != 1000) {
scheduleChatReconnect();
}
}
});
}
/**
* 连接在线人数WebSocket
*/
private void connectOnlineCountWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 停止之前的重连任务
stopOnlineReconnect();
// 构建WebSocket URL添加clientId参数
String userIdStr = AuthStore.getUserId(this);
String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
userIdStr :
"guest_" + System.currentTimeMillis();
String wsUrl = getWsOnlineBaseUrl() + roomId + "?clientId=" + clientId;
onlineCountWsClient = new OkHttpClient.Builder()
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(wsUrl)
.build();
onlineCountWebSocket = onlineCountWsClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
android.util.Log.d("OnlineWebSocket", "在线人数连接成功: roomId=" + roomId);
isOnlineWebSocketConnected = true;
onlineReconnectAttempts = 0;
// 启动心跳检测
startOnlineHeartbeat();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 收到在线人数更新消息
try {
JSONObject json = new JSONObject(text);
String type = json.optString("type", "");
if ("online_count".equals(type)) {
int count = json.optInt("count", 0);
android.util.Log.d("OnlineWebSocket", "收到在线人数更新: " + count);
handler.post(() -> {
// 更新UI显示在线人数
if (binding != null && binding.topViewerCount != null) {
binding.topViewerCount.setText(String.valueOf(count));
}
});
} else if ("connected".equals(type)) {
String message = json.optString("message", "");
android.util.Log.d("OnlineWebSocket", "连接确认: " + message);
} else if ("pong".equals(type)) {
// 收到心跳响应
android.util.Log.d("OnlineWebSocket", "收到心跳响应");
}
} catch (JSONException e) {
android.util.Log.e("OnlineWebSocket", "解析消息失败: " + e.getMessage());
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
android.util.Log.e("OnlineWebSocket", "连接失败: " + t.getMessage());
isOnlineWebSocketConnected = false;
stopOnlineHeartbeat();
// 尝试重连
scheduleOnlineReconnect();
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
android.util.Log.d("OnlineWebSocket", "连接关闭: " + reason);
isOnlineWebSocketConnected = false;
stopOnlineHeartbeat();
// 非正常关闭时尝试重连
if (code != 1000) {
scheduleOnlineReconnect();
}
}
});
}
/**
* 启动弹幕心跳检测
*/
private void startChatHeartbeat() {
stopChatHeartbeat();
chatHeartbeatRunnable = new Runnable() {
@Override
public void run() {
if (chatWebSocket != null && isChatWebSocketConnected) {
try {
JSONObject ping = new JSONObject();
ping.put("type", "ping");
chatWebSocket.send(ping.toString());
android.util.Log.d("ChatWebSocket", "发送心跳");
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送心跳失败: " + e.getMessage());
}
handler.postDelayed(chatHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
}
};
handler.postDelayed(chatHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
/**
* 停止弹幕心跳检测
*/
private void stopChatHeartbeat() {
if (chatHeartbeatRunnable != null) {
handler.removeCallbacks(chatHeartbeatRunnable);
chatHeartbeatRunnable = null;
}
}
/**
* 启动在线人数心跳检测
*/
private void startOnlineHeartbeat() {
stopOnlineHeartbeat();
onlineHeartbeatRunnable = new Runnable() {
@Override
public void run() {
if (onlineCountWebSocket != null && isOnlineWebSocketConnected) {
try {
JSONObject ping = new JSONObject();
ping.put("type", "ping");
onlineCountWebSocket.send(ping.toString());
android.util.Log.d("OnlineWebSocket", "发送心跳");
} catch (JSONException e) {
android.util.Log.e("OnlineWebSocket", "发送心跳失败: " + e.getMessage());
}
handler.postDelayed(onlineHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
}
};
handler.postDelayed(onlineHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
/**
* 停止在线人数心跳检测
*/
private void stopOnlineHeartbeat() {
if (onlineHeartbeatRunnable != null) {
handler.removeCallbacks(onlineHeartbeatRunnable);
onlineHeartbeatRunnable = null;
}
}
/**
* 安排弹幕重连
*/
private void scheduleChatReconnect() {
if (isFinishing() || isDestroyed()) return;
if (chatReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
android.util.Log.w("ChatWebSocket", "达到最大重连次数,停止重连");
handler.post(() -> {
addChatMessage(new ChatMessage("弹幕连接断开,请刷新页面重试", true));
});
return;
}
chatReconnectAttempts++;
long delay = RECONNECT_DELAY * chatReconnectAttempts; // 递增延迟
android.util.Log.d("ChatWebSocket", "将在 " + delay + "ms 后尝试第 " + chatReconnectAttempts + " 次重连");
chatReconnectRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
connectChatWebSocket();
}
};
handler.postDelayed(chatReconnectRunnable, delay);
}
/**
* 停止弹幕重连
*/
private void stopChatReconnect() {
if (chatReconnectRunnable != null) {
handler.removeCallbacks(chatReconnectRunnable);
chatReconnectRunnable = null;
}
}
/**
* 安排在线人数重连
*/
private void scheduleOnlineReconnect() {
if (isFinishing() || isDestroyed()) return;
if (onlineReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
android.util.Log.w("OnlineWebSocket", "达到最大重连次数,停止重连");
return;
}
onlineReconnectAttempts++;
long delay = RECONNECT_DELAY * onlineReconnectAttempts; // 递增延迟
android.util.Log.d("OnlineWebSocket", "将在 " + delay + "ms 后尝试第 " + onlineReconnectAttempts + " 次重连");
onlineReconnectRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
connectOnlineCountWebSocket();
}
};
handler.postDelayed(onlineReconnectRunnable, delay);
}
/**
* 停止在线人数重连
*/
private void stopOnlineReconnect() {
if (onlineReconnectRunnable != null) {
handler.removeCallbacks(onlineReconnectRunnable);
onlineReconnectRunnable = null;
}
}
private void sendChatViaWebSocket(String content) {
if (chatWebSocket == null || !isChatWebSocketConnected) {
// 如果 WebSocket 未连接,先本地显示
addChatMessage(new ChatMessage("", content));
Toast.makeText(this, "弹幕连接断开,消息仅本地显示", Toast.LENGTH_SHORT).show();
return;
}
try {
JSONObject json = new JSONObject();
json.put("type", "chat");
json.put("content", content);
json.put("nickname", AuthStore.getNickname(this));
json.put("userId", AuthStore.getUserId(this));
chatWebSocket.send(json.toString());
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送消息失败: " + e.getMessage());
// 失败时本地显示
addChatMessage(new ChatMessage("", content));
}
}
private void disconnectWebSocket() {
// 断开弹幕WebSocket
stopChatHeartbeat();
stopChatReconnect();
isChatWebSocketConnected = false;
if (chatWebSocket != null) {
chatWebSocket.close(1000, "Activity destroyed");
chatWebSocket = null;
}
if (chatWsClient != null) {
chatWsClient.dispatcher().executorService().shutdown();
chatWsClient = null;
}
// 断开在线人数WebSocket
stopOnlineHeartbeat();
stopOnlineReconnect();
isOnlineWebSocketConnected = false;
if (onlineCountWebSocket != null) {
onlineCountWebSocket.close(1000, "Activity destroyed");
onlineCountWebSocket = null;
}
if (onlineCountWsClient != null) {
onlineCountWsClient.dispatcher().executorService().shutdown();
onlineCountWsClient = null;
}
}
private void addChatMessage(ChatMessage message) {
// 限制消息数量,防止内存溢出
if (chatMessages.size() > 100) {
chatMessages.remove(0);
}
chatMessages.add(message);
chatAdapter.submitList(new ArrayList<>(chatMessages));
// 滚动到最新消息
if (binding != null && binding.chatRecyclerView != null && chatMessages.size() > 0) {
binding.chatRecyclerView.scrollToPosition(chatMessages.size() - 1);
}
}
private void toggleFullscreen() {
if (isFullscreen) {
// 退出全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// ActionBar可能为null需要检查
androidx.appcompat.app.ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
}
isFullscreen = false;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
} 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();
}
isFullscreen = true;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
@Override
protected void onStart() {
super.onStart();
startPolling();
connectWebSocket(); // 连接 WebSocket
loadHistoryMessages(); // 加载历史弹幕
}
@Override
protected void onStop() {
super.onStop();
// 更新观看时长
updateWatchDuration();
stopPolling();
disconnectWebSocket(); // 断开 WebSocket
releasePlayer();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
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);
binding.exitFullscreenButton.setVisibility(View.VISIBLE);
} else {
// 竖屏时显示所有UI元素
binding.topBar.setVisibility(View.VISIBLE);
binding.roomInfoLayout.setVisibility(View.VISIBLE);
binding.chatInputLayout.setVisibility(View.VISIBLE);
binding.chatRecyclerView.setVisibility(View.VISIBLE);
binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
private void startPolling() {
stopPolling();
// 首次立即获取房间信息
fetchRoom();
pollRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
fetchRoom();
handler.postDelayed(pollRunnable, 15000); // 15秒轮询一次减少压力
}
};
// 延迟15秒后开始轮询
handler.postDelayed(pollRunnable, 15000);
}
private void stopPolling() {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
pollRunnable = null;
}
}
/**
* 记录观看历史(进入直播间时调用)
*/
private void recordWatchHistory() {
// 记录进入时间
enterTime = System.currentTimeMillis();
android.util.Log.d("RoomDetail", "【观看历史】进入直播间,记录开始时间: " + enterTime);
try {
if (!AuthHelper.isLoggedIn(this)) {
android.util.Log.w("RoomDetail", "【观看历史】用户未登录,跳过记录");
return;
}
if (TextUtils.isEmpty(roomId)) {
android.util.Log.w("RoomDetail", "【观看历史】roomId为空跳过记录");
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("targetType", "room");
body.put("targetId", roomId);
body.put("targetTitle", "直播间");
body.put("duration", 0);
apiService.recordViewHistoryNew(body).enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "【观看历史】✅ 进入记录成功");
updateWatchHistoryWithRoomInfo();
}
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "【观看历史】进入记录失败: " + t.getMessage());
}
});
} catch (Exception e) {
android.util.Log.e("RoomDetail", "【观看历史】异常: " + e.getMessage());
}
}
/**
* 更新观看时长(离开直播间时调用)
*/
private void updateWatchDuration() {
if (enterTime == 0 || !AuthHelper.isLoggedIn(this) || TextUtils.isEmpty(roomId)) {
return;
}
// 计算观看时长(秒)
int duration = (int) ((System.currentTimeMillis() - enterTime) / 1000);
android.util.Log.d("RoomDetail", "【观看历史】离开直播间,观看时长: " + duration + "");
if (duration < 1) {
return; // 观看时间太短,不记录
}
try {
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("targetType", "room");
body.put("targetId", roomId);
body.put("targetTitle", room != null ? room.getTitle() : "直播间");
body.put("duration", duration);
// 同步调用确保在Activity销毁前完成
apiService.recordViewHistoryNew(body).enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "【观看历史】✅ 时长更新成功: " + duration + "");
} else {
android.util.Log.w("RoomDetail", "【观看历史】时长更新失败");
}
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "【观看历史】时长更新失败: " + t.getMessage());
}
});
} catch (Exception e) {
android.util.Log.e("RoomDetail", "【观看历史】异常: " + e.getMessage());
}
}
/**
* 房间信息加载后更新观看历史
*/
private void updateWatchHistoryWithRoomInfo() {
if (room == null || !AuthHelper.isLoggedIn(this)) {
return;
}
try {
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("targetType", "room");
body.put("targetId", roomId);
body.put("targetTitle", room.getTitle() != null ? room.getTitle() : "直播间");
body.put("coverImage", room.getCoverImage());
body.put("streamerName", room.getStreamerName());
body.put("duration", 0);
apiService.recordViewHistoryNew(body).enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "观看历史更新成功");
}
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
// 忽略错误
}
});
} catch (Exception e) {
// 忽略所有异常
}
}
/**
* 加载历史弹幕消息
*/
private void loadHistoryMessages() {
if (TextUtils.isEmpty(roomId)) return;
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<List<ChatMessageResponse>>> call = apiService.getRoomMessages(roomId, 50);
call.enqueue(new Callback<ApiResponse<List<ChatMessageResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<ChatMessageResponse>>> call,
Response<ApiResponse<List<ChatMessageResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<ChatMessageResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<ChatMessageResponse> messages = apiResponse.getData();
// 添加欢迎消息
if (messages.isEmpty()) {
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
// 添加历史消息
for (ChatMessageResponse msg : messages) {
String nickname = msg.getNickname();
String content = msg.getContent();
if (nickname != null && content != null) {
addChatMessage(new ChatMessage(nickname, content));
}
}
android.util.Log.d("RoomDetail", "加载了 " + messages.size() + " 条历史弹幕");
} else {
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
} else {
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
}
@Override
public void onFailure(Call<ApiResponse<List<ChatMessageResponse>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "加载历史弹幕失败: " + t.getMessage());
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
});
}
private boolean isFirstLoad = true;
private void fetchRoom() {
android.util.Log.d("RoomDetail", "fetchRoom() roomId=" + roomId);
if (TextUtils.isEmpty(roomId)) {
android.util.Log.e("RoomDetail", "roomId 为空,退出");
Toast.makeText(this, "房间不存在", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 只在首次加载时显示loading
if (isFirstLoad) {
binding.loadingIndicator.setVisibility(View.VISIBLE);
}
ApiClient.getService(getApplicationContext()).getRoom(roomId).enqueue(new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (isFinishing() || isDestroyed()) return;
binding.loadingIndicator.setVisibility(View.GONE);
boolean firstLoad = isFirstLoad;
isFirstLoad = false;
android.util.Log.d("RoomDetail", "onResponse: code=" + response.code() + ", firstLoad=" + firstLoad);
ApiResponse<Room> body = response.body();
Room data = body != null ? body.getData() : null;
android.util.Log.d("RoomDetail", "body=" + (body != null) + ", isOk=" + (body != null && body.isOk()) + ", data=" + (data != null));
if (!response.isSuccessful() || body == null || !body.isOk() || data == null) {
String msg = body != null && !TextUtils.isEmpty(body.getMessage()) ? body.getMessage() : "房间不存在";
android.util.Log.e("RoomDetail", "API 失败: " + msg);
// 不要在首次加载失败时退出,让用户可以看到错误
Toast.makeText(RoomDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
return;
}
room = data;
android.util.Log.d("RoomDetail", "房间加载成功: " + room.getTitle());
bindRoom(room);
// 房间信息加载后更新观看历史
updateWatchHistoryWithRoomInfo();
}
@Override
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
if (isFinishing() || isDestroyed()) return;
binding.loadingIndicator.setVisibility(View.GONE);
isFirstLoad = false;
android.util.Log.e("RoomDetail", "onFailure: " + t.getMessage());
}
});
}
private void shareRoom() {
if (room == null || roomId == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
String shareLink = ShareUtils.generateRoomShareLink(roomId);
String title = room.getTitle() != null ? room.getTitle() : "直播间";
String text = "来看看这个精彩的直播:" + title;
ShareUtils.shareLink(this, shareLink, title, text);
}
private void bindRoom(Room r) {
try {
String title = r.getTitle() != null ? r.getTitle() : "直播间";
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
// 设置顶部标题栏
binding.topTitle.setText(title);
// 设置房间信息区域
binding.roomTitle.setText(title);
binding.streamerName.setText(streamer);
// 设置直播状态
if (r.isLive()) {
binding.liveTag.setVisibility(View.VISIBLE);
binding.offlineLayout.setVisibility(View.GONE);
// 在线人数已通过WebSocket实时更新这里只设置初始值
if (r.getViewerCount() > 0) {
binding.topViewerCount.setText(String.valueOf(r.getViewerCount()));
}
// 如果后端返回的viewerCount为0保持UI不变等待WebSocket推送
} else {
binding.liveTag.setVisibility(View.GONE);
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
return;
}
// 获取播放地址
// 优先使用 HTTP-FLVIjkPlayer延迟更低2-3秒
// HLS 作为备用(延迟 6-10秒
String playUrl = null;
String fallbackUrl = null;
if (r.getStreamUrls() != null) {
// 打印流地址信息用于调试
android.util.Log.d("RoomDetail", "流地址信息:");
android.util.Log.d("RoomDetail", " FLV: " + r.getStreamUrls().getFlv());
android.util.Log.d("RoomDetail", " HLS: " + r.getStreamUrls().getHls());
android.util.Log.d("RoomDetail", " RTMP: " + r.getStreamUrls().getRtmp());
// 优先使用 FLV低延迟
playUrl = r.getStreamUrls().getFlv();
fallbackUrl = r.getStreamUrls().getHls();
// 如果 FLV 不可用,使用 HLS
if (TextUtils.isEmpty(playUrl)) {
android.util.Log.w("RoomDetail", "FLV 地址为空,使用 HLS");
playUrl = fallbackUrl;
fallbackUrl = null;
}
} else {
android.util.Log.e("RoomDetail", "streamUrls 为 null");
}
android.util.Log.d("RoomDetail", "最终播放地址: " + playUrl);
android.util.Log.d("RoomDetail", "备用地址: " + fallbackUrl);
if (!TextUtils.isEmpty(playUrl)) {
ensurePlayer(playUrl, fallbackUrl);
} else {
// 没有播放地址时显示离线状态
android.util.Log.e("RoomDetail", "没有可用的播放地址,显示离线状态");
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
}
} catch (Exception e) {
android.util.Log.e("RoomDetail", "bindRoom异常: " + e.getMessage(), e);
// 不要因为绑定失败就退出,显示错误状态
if (binding != null && binding.offlineLayout != null) {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
private void ensurePlayer(String url, String fallbackHlsUrl) {
try {
if (TextUtils.isEmpty(url)) return;
if (url.endsWith(".flv")) {
if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return;
startFlv(url, fallbackHlsUrl);
return;
}
if (player != null) {
MediaItem current = player.getCurrentMediaItem();
String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null
? current.localConfiguration.uri.toString()
: null;
if (currentUri != null && currentUri.equals(url)) return;
}
startHls(url, null);
} catch (Exception e) {
android.util.Log.e("RoomDetail", "ensurePlayer异常: " + e.getMessage(), e);
// 播放器初始化失败,显示离线状态
if (binding != null && binding.offlineLayout != null) {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
// 防止重复显示连接消息
private boolean hasShownConnectedMessage = false;
@OptIn(markerClass = UnstableApi.class) private void startHls(String url, @Nullable String altUrl) {
releaseIjkPlayer();
if (binding != null) {
binding.flvTextureView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
}
releaseExoPlayer();
triedAltUrl = false;
hasShownConnectedMessage = false; // 重置连接消息标志
// 优化缓冲配置 - 针对低延迟 HLS1秒分片
// 平衡延迟和流畅度
androidx.media3.exoplayer.DefaultLoadControl loadControl =
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
.setBufferDurationsMs(
2500, // 最小缓冲 2.5秒约2-3个分片
10000, // 最大缓冲 10秒
1500, // 播放前缓冲 1.5秒(快速起播)
2500 // 重新缓冲 2.5秒
)
.setPrioritizeTimeOverSizeThresholds(true)
.build();
// 创建播放器
ExoPlayer exo = new ExoPlayer.Builder(this)
.setLoadControl(loadControl)
.build();
// 关键:设置为直播模式,自动跳到最新位置
exo.setPlayWhenReady(true);
// 设置播放器视图
binding.playerView.setPlayer(exo);
binding.playerView.setUseController(true);
binding.playerView.setControllerAutoShow(false);
String computedAltUrl = altUrl;
if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url);
String finalComputedAltUrl = computedAltUrl;
final String finalUrl = url;
exo.addListener(new Player.Listener() {
private int retryCount = 0;
private static final int MAX_RETRY = 3;
@Override
public void onPlayerError(PlaybackException error) {
android.util.Log.e("ExoPlayer", "播放错误: " + error.getMessage());
// 先尝试备用地址
if (!triedAltUrl && !TextUtils.isEmpty(finalComputedAltUrl)) {
triedAltUrl = true;
android.util.Log.d("ExoPlayer", "尝试备用地址: " + finalComputedAltUrl);
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
exo.prepare();
exo.setPlayWhenReady(true);
return;
}
// 自动重试
if (retryCount < MAX_RETRY) {
retryCount++;
android.util.Log.d("ExoPlayer", "自动重试 " + retryCount + "/" + MAX_RETRY);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
exo.setMediaItem(MediaItem.fromUri(finalUrl));
exo.prepare();
exo.setPlayWhenReady(true);
}
}, 2000);
return;
}
// 重试失败,显示离线状态
binding.offlineLayout.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
fetchRoom();
}
}, 5000);
}
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY) {
binding.offlineLayout.setVisibility(View.GONE);
retryCount = 0;
// 关键:跳到直播流的最新位置,减少延迟
if (exo.isCurrentMediaItemLive()) {
exo.seekToDefaultPosition();
}
// 只显示一次连接消息
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
} else if (playbackState == Player.STATE_BUFFERING) {
android.util.Log.d("ExoPlayer", "正在缓冲...");
}
}
});
android.util.Log.d("ExoPlayer", "开始播放: " + url);
exo.setMediaItem(MediaItem.fromUri(url));
exo.prepare();
exo.setPlayWhenReady(true);
player = exo;
}
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
android.util.Log.d("RoomDetail", "播放FLV流: " + flvUrl);
// 释放其他播放器
releaseExoPlayer();
releaseGSYPlayer();
// 保存备用地址
ijkFallbackHlsUrl = fallbackHlsUrl;
ijkFallbackTried = false;
hasShownConnectedMessage = false;
// 显示 FLV 播放视图(使用 SurfaceView
if (binding != null) {
binding.playerView.setVisibility(View.GONE);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(View.VISIBLE);
}
// 使用 IjkPlayer 播放 FLV
pendingFlvUrl = flvUrl;
ijkUrl = flvUrl;
// 如果 SurfaceHolder 已经准备好,直接播放
if (ijkSurfaceHolder != null) {
prepareIjkWithHolder(flvUrl, ijkSurfaceHolder);
} else {
android.util.Log.d("RoomDetail", "等待 SurfaceHolder 准备...");
// SurfaceHolder 会在 surfaceCreated 回调中触发播放
}
}
/**
* 释放 GSYVideoPlayer
*/
private void releaseGSYPlayer() {
if (gsyPlayer != null) {
gsyPlayer.release();
}
GSYVideoManager.releaseAllVideos();
}
// SurfaceHolder.Callback 实现
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView created");
ijkSurfaceHolder = holder;
if (pendingFlvUrl != null) {
prepareIjkWithHolder(pendingFlvUrl, holder);
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
android.util.Log.d("IjkPlayer", "SurfaceView changed: " + width + "x" + height);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView destroyed");
ijkSurfaceHolder = null;
}
private void prepareIjkWithHolder(String url, SurfaceHolder holder) {
if (holder == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 关键:视频解码器设置 ==========
// 优先使用软解码(更稳定)
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);
// 视频输出格式
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1000000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 16);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOnPreparedListener(mp -> {
android.util.Log.d("IjkPlayer", "准备完成,开始播放");
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
// 获取视频宽高,根据视频宽高比调整显示方式
int videoWidth = mp.getVideoWidth();
int videoHeight = mp.getVideoHeight();
android.util.Log.d("IjkPlayer", "视频尺寸: " + videoWidth + "x" + videoHeight);
if (videoWidth > 0 && videoHeight > 0) {
// 在主线程中调整显示
handler.post(() -> adjustVideoDisplayByStreamType(videoWidth, videoHeight));
}
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
return true;
}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
p.setOnInfoListener((mp, what, extra) -> {
if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
android.util.Log.d("IjkPlayer", "视频开始渲染");
binding.offlineLayout.setVisibility(View.GONE);
}
return false;
});
ijkPlayer = p;
try {
p.setDisplay(holder);
p.setDataSource(url);
android.util.Log.d("IjkPlayer", "开始准备播放: " + url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
}
}
}
private void prepareIjk(String url) {
if (ijkSurface == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 关键:视频解码器设置 ==========
// 使用硬件解码MediaCodec
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1);
// 如果硬解失败,自动回退到软解
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-hevc", 1);
// 视频输出格式 - 使用 OpenGL ES 渲染
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5);
// ========== 音视频同步 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 0);
p.setOnPreparedListener(mp -> {
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
// 只显示一次连接消息
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
// 获取视频宽高,根据视频宽高比调整显示方式
int videoWidth = mp.getVideoWidth();
int videoHeight = mp.getVideoHeight();
android.util.Log.d("IjkPlayer", "视频尺寸: " + videoWidth + "x" + videoHeight);
if (videoWidth > 0 && videoHeight > 0) {
// 在主线程中调整显示
handler.post(() -> adjustVideoDisplayByStreamType(videoWidth, videoHeight));
}
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
fetchRoom();
}
}, 5000);
return true;
}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
// 添加缓冲监听
p.setOnInfoListener((mp, what, extra) -> {
if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_START) {
android.util.Log.d("IjkPlayer", "开始缓冲...");
// 可以显示加载指示器
} else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_END) {
android.util.Log.d("IjkPlayer", "缓冲结束");
// 隐藏加载指示器
} else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
android.util.Log.d("IjkPlayer", "视频开始渲染");
binding.offlineLayout.setVisibility(View.GONE);
}
return false;
});
ijkPlayer = p;
try {
p.setSurface(ijkSurface);
p.setDataSource(url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
} else {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
private static boolean ijkLibLoadFailed = false;
private static void ensureIjkLibsLoaded() {
if (ijkLibLoaded || ijkLibLoadFailed) {
android.util.Log.d("IjkPlayer", "IjkPlayer 库状态: loaded=" + ijkLibLoaded + ", failed=" + ijkLibLoadFailed);
return;
}
try {
// 检查设备 CPU 架构
String[] abis = android.os.Build.SUPPORTED_ABIS;
android.util.Log.d("IjkPlayer", "设备 CPU 架构: " + java.util.Arrays.toString(abis));
boolean supported = false;
for (String abi : abis) {
if ("armeabi-v7a".equals(abi) || "arm64-v8a".equals(abi) || "x86".equals(abi) || "x86_64".equals(abi)) {
supported = true;
break;
}
}
if (!supported) {
android.util.Log.w("IjkPlayer", "设备 CPU 架构不支持 IjkPlayer: " + java.util.Arrays.toString(abis));
ijkLibLoadFailed = true;
return;
}
IjkMediaPlayer.loadLibrariesOnce(null);
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
ijkLibLoaded = true;
android.util.Log.d("IjkPlayer", "IjkPlayer 库加载成功");
} catch (Throwable e) {
android.util.Log.e("IjkPlayer", "加载IjkPlayer库失败: " + e.getMessage(), e);
ijkLibLoadFailed = true;
}
}
private void releasePlayer() {
releaseExoPlayer();
releaseIjkPlayer();
releaseGSYPlayer();
pendingFlvUrl = null;
if (binding != null) {
binding.playerView.setPlayer(null);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
binding.gsyPlayer.setVisibility(View.GONE);
}
}
private void releaseExoPlayer() {
if (player != null) {
player.release();
player = null;
}
}
private void releaseIjkPlayer() {
if (ijkPlayer != null) {
try {
ijkPlayer.stop();
ijkPlayer.reset();
ijkPlayer.release();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "释放播放器失败: " + e.getMessage());
}
ijkPlayer = null;
}
if (ijkSurface != null) {
ijkSurface.release();
ijkSurface = null;
}
ijkSurfaceHolder = null;
ijkUrl = null;
ijkFallbackHlsUrl = null;
ijkFallbackTried = false;
}
private String getAltHlsUrl(String url) {
if (url == null) return null;
if (!url.endsWith(".m3u8")) return null;
if (url.contains("/index.m3u8")) return null;
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
@Override
protected void onPause() {
super.onPause();
// GSYVideoPlayer 暂停
if (gsyPlayer != null) {
gsyPlayer.onVideoPause();
}
}
@Override
protected void onResume() {
super.onResume();
// GSYVideoPlayer 恢复
if (gsyPlayer != null) {
gsyPlayer.onVideoResume();
}
}
@Override
public void onBackPressed() {
// GSYVideoPlayer 返回处理
if (GSYVideoManager.backFromWindowFull(this)) {
return;
}
super.onBackPressed();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 确保Handler回调被清理防止内存泄漏
if (handler != null) {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
}
// 清理弹幕心跳和重连回调
if (chatHeartbeatRunnable != null) {
handler.removeCallbacks(chatHeartbeatRunnable);
}
if (chatReconnectRunnable != null) {
handler.removeCallbacks(chatReconnectRunnable);
}
// 清理在线人数心跳和重连回调
if (onlineHeartbeatRunnable != null) {
handler.removeCallbacks(onlineHeartbeatRunnable);
}
if (onlineReconnectRunnable != null) {
handler.removeCallbacks(onlineReconnectRunnable);
}
}
// 断开 WebSocket 连接
disconnectWebSocket();
// 释放播放器资源
releasePlayer();
// 释放礼物弹窗
if (giftDialog != null && giftDialog.isShowing()) {
giftDialog.dismiss();
}
}
/**
* 初始化礼物列表
*/
private void setupGifts() {
// 从后端加载礼物列表
loadGiftsFromBackend();
}
/**
* 从后端加载礼物列表
*/
private void loadGiftsFromBackend() {
android.util.Log.d("RoomDetail", "=== 开始加载礼物列表 ===");
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
call.enqueue(new Callback<ApiResponse<List<GiftResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<GiftResponse>>> call,
Response<ApiResponse<List<GiftResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<GiftResponse>> apiResponse = response.body();
android.util.Log.d("RoomDetail", "礼物列表响应码: " + apiResponse.getCode());
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
availableGifts = new ArrayList<>();
for (GiftResponse giftResponse : apiResponse.getData()) {
// 使用URL构造函数从服务器加载图标
String iconUrl = giftResponse.getIconUrl();
android.util.Log.d("RoomDetail", "礼物: " + giftResponse.getName() +
", 价格: " + giftResponse.getPrice() +
", iconUrl: " + iconUrl);
Gift gift = new Gift(
String.valueOf(giftResponse.getId()),
giftResponse.getName(),
giftResponse.getPrice().intValue(),
iconUrl, // ✅ 使用URL而不是硬编码
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
);
availableGifts.add(gift);
}
android.util.Log.d("RoomDetail", "✅ 成功加载 " + availableGifts.size() + " 个礼物");
android.util.Log.d("RoomDetail", "礼物列表: " + getGiftNames());
} else {
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败: " + apiResponse.getMessage());
setDefaultGifts();
}
} else {
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败,响应码: " + response.code());
setDefaultGifts();
}
}
@Override
public void onFailure(Call<ApiResponse<List<GiftResponse>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "❌ 加载礼物列表网络错误: " + t.getMessage(), t);
setDefaultGifts();
}
});
}
/**
* 设置默认礼物列表(当接口失败时使用)
*/
private void setDefaultGifts() {
availableGifts = new ArrayList<>();
// 不再使用模拟数据,只从后端接口获取真实礼物数据
}
/**
* 加载礼物图标到ImageView支持PNG和SVG格式
* @param imageView 目标ImageView
* @param gift 礼物对象
*/
private void loadGiftIcon(android.widget.ImageView imageView, Gift gift) {
if (gift.hasIconUrl()) {
String iconUrl = gift.getIconUrl();
android.util.Log.d("RoomDetail", "加载礼物图标: " + gift.getName() + ", URL: " + iconUrl);
// 检查是否是SVG格式
if (iconUrl.toLowerCase().endsWith(".svg") || iconUrl.toLowerCase().contains(".svg?")) {
// 加载SVG格式
com.bumptech.glide.RequestBuilder<android.graphics.drawable.PictureDrawable> requestBuilder =
com.bumptech.glide.Glide.with(this)
.as(android.graphics.drawable.PictureDrawable.class)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.PictureDrawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
Object model,
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
boolean isFirstResource) {
android.util.Log.e("RoomDetail", "SVG加载失败: " + gift.getName(), e);
imageView.setImageResource(R.drawable.ic_gift_24);
return true;
}
@Override
public boolean onResourceReady(android.graphics.drawable.PictureDrawable resource,
Object model,
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
com.bumptech.glide.load.DataSource dataSource,
boolean isFirstResource) {
android.util.Log.d("RoomDetail", "SVG加载成功: " + gift.getName());
return false;
}
});
requestBuilder.load(iconUrl).into(new com.example.livestreaming.glide.SvgSoftwareLayerSetter(imageView));
} else {
// 加载PNG/JPG等格式
com.bumptech.glide.Glide.with(this)
.load(iconUrl)
.placeholder(R.drawable.ic_gift_24)
.error(R.drawable.ic_gift_24)
.diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy.ALL)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
Object model,
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
boolean isFirstResource) {
android.util.Log.e("RoomDetail", "图片加载失败: " + gift.getName(), e);
return false;
}
@Override
public boolean onResourceReady(android.graphics.drawable.Drawable resource,
Object model,
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
com.bumptech.glide.load.DataSource dataSource,
boolean isFirstResource) {
android.util.Log.d("RoomDetail", "图片加载成功: " + gift.getName());
return false;
}
})
.into(imageView);
}
} else {
// 如果没有URL使用本地资源
imageView.setImageResource(gift.getIconResId());
}
}
/**
* 显示礼物选择弹窗
*/
private void showGiftDialog() {
// 检查登录状态
if (!AuthHelper.requireLoginWithToast(this, "送礼物需要登录")) {
return;
}
if (giftDialog == null) {
giftDialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_gift, null);
giftDialog.setContentView(view);
// 初始化礼物列表
androidx.recyclerview.widget.RecyclerView giftRecyclerView = view.findViewById(R.id.giftRecyclerView);
giftRecyclerView.setLayoutManager(new GridLayoutManager(this, 4));
giftAdapter = new GiftAdapter();
giftAdapter.setGifts(availableGifts);
giftRecyclerView.setAdapter(giftAdapter);
// 金币余额
android.widget.TextView coinBalance = view.findViewById(R.id.coinBalance);
// 从后端加载用户金币余额
loadUserBalance(coinBalance);
// 选中的礼物信息
android.widget.LinearLayout selectedGiftLayout = view.findViewById(R.id.selectedGiftLayout);
android.widget.ImageView selectedGiftIcon = view.findViewById(R.id.selectedGiftIcon);
android.widget.TextView selectedGiftName = view.findViewById(R.id.selectedGiftName);
android.widget.TextView selectedGiftPrice = view.findViewById(R.id.selectedGiftPrice);
// 数量控制
android.widget.TextView giftCountText = view.findViewById(R.id.giftCount);
android.widget.ImageButton decreaseButton = view.findViewById(R.id.decreaseButton);
android.widget.ImageButton increaseButton = view.findViewById(R.id.increaseButton);
final int[] giftCount = {1}; // 使用数组以便在lambda中修改
// 礼物选择监听
giftAdapter.setOnGiftClickListener(gift -> {
selectedGiftLayout.setVisibility(View.VISIBLE);
// 使用辅助方法加载礼物图标
loadGiftIcon(selectedGiftIcon, gift);
selectedGiftName.setText(gift.getName());
selectedGiftPrice.setText(gift.getFormattedPrice());
giftCount[0] = 1;
giftCountText.setText(String.valueOf(giftCount[0]));
});
// 减少数量
decreaseButton.setOnClickListener(v -> {
if (giftCount[0] > 1) {
giftCount[0]--;
giftCountText.setText(String.valueOf(giftCount[0]));
}
});
// 增加数量
increaseButton.setOnClickListener(v -> {
if (giftCount[0] < 99) {
giftCount[0]++;
giftCountText.setText(String.valueOf(giftCount[0]));
}
});
// 赠送按钮
com.google.android.material.button.MaterialButton sendGiftButton = view.findViewById(R.id.sendGiftButton);
sendGiftButton.setOnClickListener(v -> {
Gift selectedGift = giftAdapter.getSelectedGift();
if (selectedGift == null) {
Toast.makeText(this, "请选择礼物", Toast.LENGTH_SHORT).show();
return;
}
int totalPrice = selectedGift.getPrice() * giftCount[0];
if (totalPrice > userCoinBalance) {
Toast.makeText(this, "金币余额不足,请充值", Toast.LENGTH_SHORT).show();
// 显示充值对话框
showRechargeDialog();
return;
}
// 调用后端接口赠送礼物
sendGiftToBackend(selectedGift, giftCount[0], totalPrice, coinBalance, selectedGiftLayout, giftCountText);
});
// 关闭按钮
view.findViewById(R.id.closeButton).setOnClickListener(v -> giftDialog.dismiss());
// 充值按钮
view.findViewById(R.id.rechargeButton).setOnClickListener(v -> showRechargeDialog());
}
giftDialog.show();
}
/**
* 显示充值对话框
*/
private void showRechargeDialog() {
// 跳转到钱包页面进行充值
Intent intent = new Intent(this, WalletActivity.class);
startActivity(intent);
}
/**
* 加载充值选项列表
*/
private void loadRechargeOptions(RechargeAdapter adapter, View dialogView) {
ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<List<RechargeOptionResponse>>> call = apiService.getRechargeOptions();
call.enqueue(new Callback<ApiResponse<List<RechargeOptionResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<RechargeOptionResponse>>> call,
Response<ApiResponse<List<RechargeOptionResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<RechargeOptionResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<RechargeOption> options = new ArrayList<>();
for (RechargeOptionResponse optionResponse : apiResponse.getData()) {
RechargeOption option = new RechargeOption(
optionResponse.getId(),
optionResponse.getCoinAmount().intValue(),
optionResponse.getPrice().doubleValue(),
optionResponse.getDiscountLabel()
);
options.add(option);
}
adapter.setOptions(options);
} else {
Toast.makeText(RoomDetailActivity.this,
"加载充值选项失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
} else {
Toast.makeText(RoomDetailActivity.this,
"加载充值选项失败",
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
}
@Override
public void onFailure(Call<ApiResponse<List<RechargeOptionResponse>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
});
}
/**
* 设置默认充值选项(当接口失败时使用)
*/
private void setDefaultRechargeOptions(RechargeAdapter adapter) {
List<RechargeOption> rechargeOptions = new ArrayList<>();
rechargeOptions.add(new RechargeOption("1", 100, 10.0, "首充优惠"));
rechargeOptions.add(new RechargeOption("2", 300, 30.0));
rechargeOptions.add(new RechargeOption("3", 500, 50.0, "热门"));
rechargeOptions.add(new RechargeOption("4", 1000, 100.0));
rechargeOptions.add(new RechargeOption("5", 3000, 300.0, "最划算"));
rechargeOptions.add(new RechargeOption("6", 5000, 500.0));
adapter.setOptions(rechargeOptions);
}
/**
* 创建充值订单
*/
private void createRechargeOrder(RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = ApiClient.getService(this);
CreateRechargeRequest request = new CreateRechargeRequest(
Integer.parseInt(selectedOption.getId()),
new java.math.BigDecimal(selectedOption.getCoinAmount()),
new java.math.BigDecimal(selectedOption.getPrice())
);
Call<ApiResponse<CreateRechargeResponse>> call = apiService.createRecharge(request);
call.enqueue(new Callback<ApiResponse<CreateRechargeResponse>>() {
@Override
public void onResponse(Call<ApiResponse<CreateRechargeResponse>> call,
Response<ApiResponse<CreateRechargeResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<CreateRechargeResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
CreateRechargeResponse rechargeResponse = apiResponse.getData();
String orderId = rechargeResponse.getOrderId();
String paymentUrl = rechargeResponse.getPaymentUrl();
// 显示支付选择对话框
showPaymentMethodDialog(orderId, selectedOption, rechargeDialog);
} else {
Toast.makeText(RoomDetailActivity.this,
"创建充值订单失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"创建充值订单失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<CreateRechargeResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 显示支付方式选择对话框
*/
private void showPaymentMethodDialog(String orderId, RechargeOption selectedOption,
androidx.appcompat.app.AlertDialog rechargeDialog) {
String[] paymentMethods = {"支付宝支付", "微信支付"};
new MaterialAlertDialogBuilder(this)
.setTitle("选择支付方式")
.setItems(paymentMethods, (dialog, which) -> {
String payType;
String payChannel;
switch (which) {
case 0: // 支付宝
payType = "alipay";
payChannel = "appAliPay";
break;
case 1: // 微信
payType = "weixin";
payChannel = "weixinAppAndroid";
break;
default:
return;
}
// 调用支付接口
processPayment(orderId, payType, payChannel, selectedOption, rechargeDialog);
})
.setNegativeButton("取消", null)
.show();
}
/**
* 处理支付
*/
private void processPayment(String orderId, String payType, String payChannel,
RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = ApiClient.getService(this);
OrderPayRequest payRequest = new OrderPayRequest(orderId, payType, payChannel);
Call<ApiResponse<OrderPayResultResponse>> call = apiService.payment(payRequest);
call.enqueue(new Callback<ApiResponse<OrderPayResultResponse>>() {
@Override
public void onResponse(Call<ApiResponse<OrderPayResultResponse>> call,
Response<ApiResponse<OrderPayResultResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<OrderPayResultResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
OrderPayResultResponse payResult = apiResponse.getData();
// TODO: 集成支付SDK微信支付、支付宝等
// 1. 根据payType调用对应的支付SDK
// 2. 传入jsConfig参数发起支付
// 3. 监听支付结果回调
// 4. 支付成功后查询订单状态并更新余额
Toast.makeText(RoomDetailActivity.this,
"支付接口调用成功,订单号: " + payResult.getOrderNo() +
"\n请集成支付SDK完成实际支付",
Toast.LENGTH_LONG).show();
// TODO: 集成支付SDK后在支付成功回调中更新余额
// 支付成功后应该调用后端接口查询订单状态并更新余额
rechargeDialog.dismiss();
} else {
Toast.makeText(RoomDetailActivity.this,
"支付失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"支付失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<OrderPayResultResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 从后端加载用户金币余额
*/
private void loadUserBalance(android.widget.TextView coinBalanceView) {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<UserBalanceResponse>> call = apiService.getUserBalance();
call.enqueue(new Callback<ApiResponse<UserBalanceResponse>>() {
@Override
public void onResponse(Call<ApiResponse<UserBalanceResponse>> call,
Response<ApiResponse<UserBalanceResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<UserBalanceResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
userCoinBalance = apiResponse.getData().getCoinBalance().intValue();
coinBalanceView.setText(String.valueOf(userCoinBalance));
} else {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
} else {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
}
@Override
public void onFailure(Call<ApiResponse<UserBalanceResponse>> call, Throwable t) {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
});
}
/**
* 发送礼物到后端
*/
private void sendGiftToBackend(Gift selectedGift, int count, int totalPrice,
android.widget.TextView coinBalance,
android.widget.LinearLayout selectedGiftLayout,
android.widget.TextView giftCountText) {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 获取主播用户ID
Integer streamerId = room.getStreamerId();
if (streamerId == null) {
streamerId = room.getUid();
}
if (streamerId == null) {
Toast.makeText(this, "无法获取主播信息", Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "主播ID为空: roomId=" + roomId + ", room=" + room);
return;
}
// 🔍 详细日志
android.util.Log.d("RoomDetail", "========================================");
android.util.Log.d("RoomDetail", "📤 准备发送礼物请求");
android.util.Log.d("RoomDetail", "roomId: " + roomId);
android.util.Log.d("RoomDetail", "streamerId: " + streamerId);
android.util.Log.d("RoomDetail", "giftId: " + selectedGift.getId());
android.util.Log.d("RoomDetail", "count: " + count);
android.util.Log.d("RoomDetail", "API URL: " + ApiClient.getCurrentBaseUrl(getApplicationContext()) + "api/front/live/rooms/" + roomId + "/gift");
android.util.Log.d("RoomDetail", "========================================");
ApiService apiService = ApiClient.getService(getApplicationContext());
SendGiftRequest request = new SendGiftRequest(
Integer.parseInt(selectedGift.getId()),
streamerId, // receiverId - 接收者用户ID主播ID
count // giftCount - 礼物数量
);
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
call.enqueue(new Callback<ApiResponse<SendGiftResponse>>() {
@Override
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
Response<ApiResponse<SendGiftResponse>> response) {
android.util.Log.d("RoomDetail", "📥 收到响应: HTTP " + response.code());
if (response.isSuccessful() && response.body() != null) {
ApiResponse<SendGiftResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
SendGiftResponse giftResponse = apiResponse.getData();
// 🔒 安全更新余额(添加空值检查)
if (giftResponse.getNewBalance() != null) {
userCoinBalance = giftResponse.getNewBalance().intValue();
coinBalance.setText(String.valueOf(userCoinBalance));
android.util.Log.d("RoomDetail", "✅ 余额更新成功: " + userCoinBalance);
} else {
android.util.Log.w("RoomDetail", "⚠️ newBalance 为 null尝试重新加载余额");
// 降级处理:重新加载余额
loadUserBalance(coinBalance);
}
// 在聊天区显示赠送消息
String giftMessage = String.format("送出了 %d 个 %s", count, selectedGift.getName());
addChatMessage(new ChatMessage("", giftMessage, true));
// ✨ 立即显示礼物特效动画
String currentUserNickname = ""; // 可以从用户信息中获取
String currentUserAvatar = AuthStore.getAvatar(RoomDetailActivity.this); // 获取当前用户头像
android.util.Log.d("RoomDetail", "🎁 礼物发送成功,显示特效动画");
android.util.Log.d("RoomDetail", "礼物图标URL: " + selectedGift.getIconUrl());
android.util.Log.d("RoomDetail", "用户头像URL: " + currentUserAvatar);
// 使用带头像的方法
if (giftAnimationManager != null) {
giftAnimationManager.showGiftAnimation(
selectedGift.getName(),
selectedGift.getPrice(),
count,
currentUserNickname,
selectedGift.getIconUrl(),
currentUserAvatar
);
}
if (giftDialog != null) {
giftDialog.dismiss();
}
// 重置选择
if (giftAdapter != null) {
giftAdapter.setSelectedGift(null);
}
selectedGiftLayout.setVisibility(View.GONE);
giftCountText.setText("1");
} else {
String errorMsg = apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误";
Toast.makeText(RoomDetailActivity.this,
"赠送失败: " + errorMsg,
Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "赠送礼物失败: " + errorMsg);
}
} else {
Toast.makeText(RoomDetailActivity.this,
"赠送失败: HTTP " + response.code(),
Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "赠送礼物HTTP错误: " + response.code());
}
}
@Override
public void onFailure(Call<ApiResponse<SendGiftResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "赠送礼物网络错误", t);
}
});
}
/**
* 加载房间信息
*/
private void loadRoomInfo() {
if (TextUtils.isEmpty(roomId)) {
return;
}
// 先检查房间是否被封禁
checkRoomBanStatus(() -> {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Room>> call = apiService.getRoom(roomId);
call.enqueue(new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Room> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
room = apiResponse.getData();
android.util.Log.d("RoomDetail", "房间信息加载成功: streamerId=" + room.getStreamerId() +
", isFollowing=" + room.getIsFollowing());
// 更新关注按钮状态
updateFollowButtonState();
} else {
android.util.Log.w("RoomDetail", "加载房间信息失败: " + apiResponse.getMessage());
}
} else {
android.util.Log.e("RoomDetail", "加载房间信息失败: response code=" + response.code());
}
}
@Override
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
android.util.Log.e("RoomDetail", "加载房间信息网络错误", t);
}
});
});
}
/**
* 检查房间封禁状态
*/
private void checkRoomBanStatus(Runnable onSuccess) {
try {
int roomIdInt = Integer.parseInt(roomId);
ApiClient.getService(getApplicationContext()).checkRoomBanStatus(roomIdInt)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null &&
response.body().getCode() == 200) {
Map<String, Object> data = response.body().getData();
if (data != null) {
Object isBannedObj = data.get("isBanned");
boolean isBanned = isBannedObj instanceof Boolean && (Boolean) isBannedObj;
if (isBanned) {
// 房间被封禁,显示提示并退出
String message = data.get("message") != null ?
data.get("message").toString() : "该直播间已被封禁";
showBanDialog(message);
return;
}
}
}
// 房间未被封禁,继续加载
if (onSuccess != null) {
onSuccess.run();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "检查房间封禁状态失败", t);
// 检查失败时继续加载(不阻塞用户)
if (onSuccess != null) {
onSuccess.run();
}
}
});
} catch (NumberFormatException e) {
// roomId不是数字跳过检查
if (onSuccess != null) {
onSuccess.run();
}
}
}
/**
* 显示封禁提示对话框
*/
private void showBanDialog(String message) {
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("直播间已封禁")
.setMessage(message)
.setCancelable(false)
.setPositiveButton("确定", (dialog, which) -> finish())
.show();
}
/**
* 更新关注按钮状态
*/
private void updateFollowButtonState() {
if (binding == null || binding.followButton == null || room == null) {
return;
}
Boolean isFollowing = room.getIsFollowing();
if (isFollowing != null && isFollowing) {
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
} else {
binding.followButton.setText("关注");
binding.followButton.setEnabled(true);
}
}
/**
* 关注主播
*/
private void followStreamerBackend() {
if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 获取主播用户ID
Integer streamerId = room.getStreamerId();
if (streamerId == null) {
Toast.makeText(this, "无法获取主播信息", Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "streamerId is null, room: " + room.getId());
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
// 使用正确的关注接口
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("userId", streamerId);
android.util.Log.d("RoomDetail", "关注主播: streamerId=" + streamerId + ", roomId=" + roomId);
Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(body);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
android.util.Log.d("RoomDetail", "关注响应: code=" + apiResponse.getCode() +
", message=" + apiResponse.getMessage() +
", data=" + apiResponse.getData());
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show();
// 更新按钮状态
if (binding != null && binding.followButton != null) {
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
}
// 更新room对象的关注状态
if (room != null) {
room.setIsFollowing(true);
}
// 关注成功后,检查是否需要显示粉丝团横幅
checkAndShowFanGroupBanner();
} else {
Toast.makeText(RoomDetailActivity.this,
"关注失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
android.util.Log.e("RoomDetail", "关注失败: response not successful, code=" + response.code());
Toast.makeText(RoomDetailActivity.this,
"关注失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "关注网络错误", t);
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 开始直播
* 接口: POST /api/front/live/room/{id}/start
*/
private void startLiveStream() {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查登录状态和权限
if (!AuthHelper.requireLoginWithToast(this, "开始直播需要登录")) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.startLiveRoom(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "直播已开始", Toast.LENGTH_SHORT).show();
// 刷新房间信息
fetchRoom();
} else {
Toast.makeText(RoomDetailActivity.this,
"开始直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"开始直播失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 结束直播
* 接口: POST /api/front/live/room/{id}/stop
*/
private void stopLiveStream() {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查登录状态和权限
if (!AuthHelper.requireLoginWithToast(this, "结束直播需要登录")) {
return;
}
// 显示确认对话框
new MaterialAlertDialogBuilder(this)
.setTitle("结束直播")
.setMessage("确定要结束直播吗?")
.setPositiveButton("确定", (dialog, which) -> {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.stopLiveRoom(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "直播已结束", Toast.LENGTH_SHORT).show();
// 刷新房间信息
fetchRoom();
} else {
Toast.makeText(RoomDetailActivity.this,
"结束直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"结束直播失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("取消", null)
.show();
}
/**
* 手动广播在线人数
* 接口: POST /api/live/online/broadcast/{roomId}
*/
private void broadcastOnlineCount() {
if (TextUtils.isEmpty(roomId)) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.broadcastOnlineCount(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
android.util.Log.d("RoomDetail", "在线人数广播成功");
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "在线人数广播失败: " + t.getMessage());
}
});
}
/**
* 显示礼物动画效果
* @param giftName 礼物名称
* @param count 礼物数量
* @param senderNickname 赠送者昵称
*/
private void showGiftAnimation(String giftName, int count, String senderNickname) {
runOnUiThread(() -> {
// 详细日志:诊断问题
android.util.Log.d("GiftAnimation", "=== 开始显示礼物动画 ===");
android.util.Log.d("GiftAnimation", "礼物名称: " + giftName);
android.util.Log.d("GiftAnimation", "数量: " + count);
android.util.Log.d("GiftAnimation", "赠送者: " + senderNickname);
android.util.Log.d("GiftAnimation", "动画管理器状态: " + (giftAnimationManager != null ? "已初始化" : "未初始化"));
android.util.Log.d("GiftAnimation", "礼物列表状态: " + (availableGifts != null ? "已加载(" + availableGifts.size() + "个)" : "未加载"));
// 检查动画管理器是否初始化
if (giftAnimationManager == null) {
android.util.Log.e("GiftAnimation", "❌ 礼物动画管理器未初始化!");
String message = senderNickname + " 送出了 " + count + "" + giftName;
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
return;
}
// 检查礼物列表是否加载
if (availableGifts == null || availableGifts.isEmpty()) {
android.util.Log.w("GiftAnimation", "⚠️ 礼物列表未加载或为空,使用默认价格显示特效");
// 使用默认价格显示特效而不是降级到Toast
giftAnimationManager.showGiftAnimation(
giftName,
100, // 默认价格:普通特效
count,
senderNickname,
null
);
return;
}
// 查找礼物信息
Gift gift = null;
for (Gift g : availableGifts) {
if (g.getName().equals(giftName)) {
gift = g;
break;
}
}
if (gift != null) {
// 使用礼物动画管理器显示特效
android.util.Log.d("GiftAnimation", "✅ 找到礼物信息: " + gift.getName() + ", 价格: " + gift.getPrice());
giftAnimationManager.showGiftAnimation(
giftName,
gift.getPrice(),
count,
senderNickname,
gift.getIconUrl()
);
android.util.Log.d("GiftAnimation",
String.format("✅ 显示礼物动画: %s x%d (价格:%d) - %s",
giftName, count, gift.getPrice(), senderNickname));
} else {
// 如果找不到礼物信息,使用默认价格显示特效
android.util.Log.w("GiftAnimation", "⚠️ 未找到礼物信息: " + giftName);
android.util.Log.w("GiftAnimation", "可用礼物列表: " + getGiftNames());
// 使用默认价格显示特效而不是降级到Toast
giftAnimationManager.showGiftAnimation(
giftName,
100, // 默认价格:普通特效
count,
senderNickname,
null
);
android.util.Log.d("GiftAnimation", "✅ 使用默认价格显示特效");
}
});
}
/**
* 获取礼物名称列表(用于调试)
*/
private String getGiftNames() {
if (availableGifts == null || availableGifts.isEmpty()) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < availableGifts.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(availableGifts.get(i).getName());
}
sb.append("]");
return sb.toString();
}
// ==================== 粉丝团横幅功能 ====================
private Room currentRoom; // 当前房间信息(用于粉丝团功能)
/**
* 检查并显示粉丝团横幅
* 在关注成功后调用
*/
private void checkAndShowFanGroupBanner() {
if (room == null || room.getStreamerId() == null) {
android.util.Log.d("FanGroup", "无法获取主播信息,跳过粉丝团检查");
return;
}
android.util.Log.d("FanGroup", "检查主播粉丝团: streamerId=" + room.getStreamerId());
// 获取主播的粉丝团信息
ApiClient.getService(this).getStreamerFanGroup(room.getStreamerId())
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> fanGroup = response.body().getData();
if (fanGroup != null && !fanGroup.isEmpty() && fanGroup.containsKey("id")) {
// 检查是否已加入粉丝团
int fanGroupId = ((Number) fanGroup.get("id")).intValue();
android.util.Log.d("FanGroup", "找到粉丝团: id=" + fanGroupId);
checkFanGroupJoinStatus(fanGroupId, fanGroup);
} else {
android.util.Log.d("FanGroup", "主播没有创建粉丝团");
}
} else {
android.util.Log.d("FanGroup", "获取粉丝团信息失败或为空");
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("FanGroup", "获取粉丝团信息失败", t);
}
});
}
/**
* 检查粉丝团加入状态
*/
private void checkFanGroupJoinStatus(int fanGroupId, Map<String, Object> fanGroup) {
ApiClient.getService(this).checkFanGroupJoined(fanGroupId)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> joinStatus = response.body().getData();
Boolean joined = (Boolean) joinStatus.get("joined");
if (joined == null || !joined) {
// 未加入,显示横幅
android.util.Log.d("FanGroup", "用户未加入粉丝团,显示横幅");
showFanGroupBanner(fanGroupId, fanGroup);
} else {
android.util.Log.d("FanGroup", "用户已加入粉丝团");
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("FanGroup", "检查粉丝团状态失败", t);
}
});
}
/**
* 显示粉丝团横幅
*/
private void showFanGroupBanner(int fanGroupId, Map<String, Object> fanGroup) {
runOnUiThread(() -> {
View bannerInclude = findViewById(R.id.fanGroupBannerInclude);
if (bannerInclude != null) {
bannerInclude.setVisibility(View.VISIBLE);
bannerInclude.setAlpha(0f);
bannerInclude.animate().alpha(1f).setDuration(300).start();
// 设置粉丝团名称
TextView tvFanGroupName = bannerInclude.findViewById(R.id.tvFanGroupName);
String fanGroupName = (String) fanGroup.get("name");
if (fanGroupName != null && tvFanGroupName != null) {
tvFanGroupName.setText("加入 " + fanGroupName);
}
// 加入按钮点击事件
com.google.android.material.button.MaterialButton btnJoin = bannerInclude.findViewById(R.id.btnJoinFanGroup);
if (btnJoin != null) {
btnJoin.setOnClickListener(v -> joinFanGroup(fanGroupId, bannerInclude));
}
// 关闭按钮点击事件
ImageButton btnClose = bannerInclude.findViewById(R.id.btnCloseBanner);
if (btnClose != null) {
btnClose.setOnClickListener(v -> hideFanGroupBanner(bannerInclude));
}
// 5秒后自动隐藏
handler.postDelayed(() -> {
if (bannerInclude.getVisibility() == View.VISIBLE) {
hideFanGroupBanner(bannerInclude);
}
}, 5000);
}
});
}
/**
* 加入粉丝团
*/
private void joinFanGroup(int fanGroupId, View bannerView) {
ApiClient.getService(this).joinFanGroup(fanGroupId)
.enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(Call<ApiResponse<String>> call,
Response<ApiResponse<String>> response) {
runOnUiThread(() -> {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Toast.makeText(RoomDetailActivity.this, "加入粉丝团成功!", Toast.LENGTH_SHORT).show();
hideFanGroupBanner(bannerView);
} else {
String msg = response.body() != null ? response.body().getMessage() : "加入失败";
Toast.makeText(RoomDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onFailure(Call<ApiResponse<String>> call, Throwable t) {
runOnUiThread(() -> {
Toast.makeText(RoomDetailActivity.this, "网络错误,请重试", Toast.LENGTH_SHORT).show();
});
}
});
}
/**
* 隐藏粉丝团横幅
*/
private void hideFanGroupBanner(View bannerView) {
if (bannerView != null) {
bannerView.animate()
.alpha(0f)
.setDuration(300)
.withEndAction(() -> bannerView.setVisibility(View.GONE))
.start();
}
}
}