3166 lines
136 KiB
Java
3166 lines
136 KiB
Java
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-FLV(IjkPlayer),延迟更低(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; // 重置连接消息标志
|
||
|
||
// 优化缓冲配置 - 针对低延迟 HLS(1秒分片)
|
||
// 平衡延迟和流畅度
|
||
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();
|
||
}
|
||
}
|
||
|
||
}
|